feat: implement function locators (#10632)

This commit is contained in:
jrandolf 2023-07-27 09:23:28 +02:00 committed by GitHub
parent 2423d4fd94
commit 6ad92f7f84
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 289 additions and 27 deletions

View File

@ -29,7 +29,7 @@ sidebar_label: API
| [JSCoverage](./puppeteer.jscoverage.md) | | | [JSCoverage](./puppeteer.jscoverage.md) | |
| [JSHandle](./puppeteer.jshandle.md) | <p>Represents a reference to a JavaScript object. Instances can be created using [Page.evaluateHandle()](./puppeteer.page.evaluatehandle.md).</p><p>Handles prevent the referenced JavaScript object from being garbage-collected unless the handle is purposely [disposed](./puppeteer.jshandle.dispose.md). JSHandles are auto-disposed when their associated frame is navigated away or the parent context gets destroyed.</p><p>Handles can be used as arguments for any evaluation function such as [Page.$eval()](./puppeteer.page._eval.md), [Page.evaluate()](./puppeteer.page.evaluate.md), and [Page.evaluateHandle()](./puppeteer.page.evaluatehandle.md). They are resolved to their referenced object.</p> | | [JSHandle](./puppeteer.jshandle.md) | <p>Represents a reference to a JavaScript object. Instances can be created using [Page.evaluateHandle()](./puppeteer.page.evaluatehandle.md).</p><p>Handles prevent the referenced JavaScript object from being garbage-collected unless the handle is purposely [disposed](./puppeteer.jshandle.dispose.md). JSHandles are auto-disposed when their associated frame is navigated away or the parent context gets destroyed.</p><p>Handles can be used as arguments for any evaluation function such as [Page.$eval()](./puppeteer.page._eval.md), [Page.evaluate()](./puppeteer.page.evaluate.md), and [Page.evaluateHandle()](./puppeteer.page.evaluatehandle.md). They are resolved to their referenced object.</p> |
| [Keyboard](./puppeteer.keyboard.md) | Keyboard provides an api for managing a virtual keyboard. The high level api is [Keyboard.type()](./puppeteer.keyboard.type.md), which takes raw characters and generates proper keydown, keypress/input, and keyup events on your page. | | [Keyboard](./puppeteer.keyboard.md) | Keyboard provides an api for managing a virtual keyboard. The high level api is [Keyboard.type()](./puppeteer.keyboard.type.md), which takes raw characters and generates proper keydown, keypress/input, and keyup events on your page. |
| [Locator](./puppeteer.locator.md) | Locators describe a strategy of locating elements and performing an action on them. If the action fails because the element is not ready for the action, the whole operation is retried. Various preconditions for a successful action are checked automatically. | | [Locator](./puppeteer.locator.md) | Locators describe a strategy of locating objects and performing an action on them. If the action fails because the object is not ready for the action, the whole operation is retried. Various preconditions for a successful action are checked automatically. |
| [Mouse](./puppeteer.mouse.md) | The Mouse class operates in main-frame CSS pixels relative to the top-left corner of the viewport. | | [Mouse](./puppeteer.mouse.md) | The Mouse class operates in main-frame CSS pixels relative to the top-left corner of the viewport. |
| [Page](./puppeteer.page.md) | <p>Page provides methods to interact with a single tab or [extension background page](https://developer.chrome.com/extensions/background_pages) in the browser.</p><p>:::note</p><p>One Browser instance might have multiple Page instances.</p><p>:::</p> | | [Page](./puppeteer.page.md) | <p>Page provides methods to interact with a single tab or [extension background page](https://developer.chrome.com/extensions/background_pages) in the browser.</p><p>:::note</p><p>One Browser instance might have multiple Page instances.</p><p>:::</p> |
| [ProductLauncher](./puppeteer.productlauncher.md) | Describes a launcher - a class that is able to create and launch a browser instance. | | [ProductLauncher](./puppeteer.productlauncher.md) | Describes a launcher - a class that is able to create and launch a browser instance. |

View File

@ -4,7 +4,7 @@ sidebar_label: Frame.locator
# Frame.locator() method # Frame.locator() method
Creates a locator for the provided `selector`. See [Locator](./puppeteer.locator.md) for details and supported actions. Creates a locator for the provided selector. See [Locator](./puppeteer.locator.md) for details and supported actions.
#### Signature: #### Signature:

View File

@ -0,0 +1,29 @@
---
sidebar_label: Frame.locator_1
---
# Frame.locator() method
Creates a locator for the provided function. See [Locator](./puppeteer.locator.md) for details and supported actions.
#### Signature:
```typescript
class Frame {
locator<Ret>(func: () => Awaitable<Ret>): Locator<Ret>;
}
```
## Parameters
| Parameter | Type | Description |
| --------- | --------------------------------------------------------- | ----------- |
| func | () =&gt; [Awaitable](./puppeteer.awaitable.md)&lt;Ret&gt; | |
**Returns:**
[Locator](./puppeteer.locator.md)&lt;Ret&gt;
## Remarks
Locators API is experimental and we will not follow semver for breaking change in the Locators API.

View File

@ -81,7 +81,8 @@ console.log(text);
| [hover(selector)](./puppeteer.frame.hover.md) | | Hovers the pointer over the center of the first element that matches the <code>selector</code>. | | [hover(selector)](./puppeteer.frame.hover.md) | | Hovers the pointer over the center of the first element that matches the <code>selector</code>. |
| [isDetached()](./puppeteer.frame.isdetached.md) | | Is<code>true</code> if the frame has been detached. Otherwise, <code>false</code>. | | [isDetached()](./puppeteer.frame.isdetached.md) | | Is<code>true</code> if the frame has been detached. Otherwise, <code>false</code>. |
| [isOOPFrame()](./puppeteer.frame.isoopframe.md) | | Is <code>true</code> if the frame is an out-of-process (OOP) frame. Otherwise, <code>false</code>. | | [isOOPFrame()](./puppeteer.frame.isoopframe.md) | | Is <code>true</code> if the frame is an out-of-process (OOP) frame. Otherwise, <code>false</code>. |
| [locator(selector)](./puppeteer.frame.locator.md) | | Creates a locator for the provided <code>selector</code>. See [Locator](./puppeteer.locator.md) for details and supported actions. | | [locator(selector)](./puppeteer.frame.locator.md) | | Creates a locator for the provided selector. See [Locator](./puppeteer.locator.md) for details and supported actions. |
| [locator(func)](./puppeteer.frame.locator_1.md) | | Creates a locator for the provided function. See [Locator](./puppeteer.locator.md) for details and supported actions. |
| [name()](./puppeteer.frame.name.md) | | The frame's <code>name</code> attribute as specified in the tag. | | [name()](./puppeteer.frame.name.md) | | The frame's <code>name</code> attribute as specified in the tag. |
| [page()](./puppeteer.frame.page.md) | | The page associated with the frame. | | [page()](./puppeteer.frame.page.md) | | The page associated with the frame. |
| [parentFrame()](./puppeteer.frame.parentframe.md) | | The parent frame, if any. Detached and main frames return <code>null</code>. | | [parentFrame()](./puppeteer.frame.parentframe.md) | | The parent frame, if any. Detached and main frames return <code>null</code>. |

View File

@ -4,7 +4,7 @@ sidebar_label: Locator
# Locator class # Locator class
Locators describe a strategy of locating elements and performing an action on them. If the action fails because the element is not ready for the action, the whole operation is retried. Various preconditions for a successful action are checked automatically. Locators describe a strategy of locating objects and performing an action on them. If the action fails because the object is not ready for the action, the whole operation is retried. Various preconditions for a successful action are checked automatically.
#### Signature: #### Signature:

View File

@ -4,7 +4,7 @@ sidebar_label: Page.locator
# Page.locator() method # Page.locator() method
Creates a locator for the provided `selector`. See [Locator](./puppeteer.locator.md) for details and supported actions. Creates a locator for the provided selector. See [Locator](./puppeteer.locator.md) for details and supported actions.
#### Signature: #### Signature:

View File

@ -0,0 +1,29 @@
---
sidebar_label: Page.locator_1
---
# Page.locator() method
Creates a locator for the provided function. See [Locator](./puppeteer.locator.md) for details and supported actions.
#### Signature:
```typescript
class Page {
locator<Ret>(func: () => Awaitable<Ret>): Locator<Ret>;
}
```
## Parameters
| Parameter | Type | Description |
| --------- | --------------------------------------------------------- | ----------- |
| func | () =&gt; [Awaitable](./puppeteer.awaitable.md)&lt;Ret&gt; | |
**Returns:**
[Locator](./puppeteer.locator.md)&lt;Ret&gt;
## Remarks
Locators API is experimental and we will not follow semver for breaking change in the Locators API.

View File

@ -118,7 +118,8 @@ page.off('request', logRequest);
| [isDragInterceptionEnabled()](./puppeteer.page.isdraginterceptionenabled.md) | | <code>true</code> if drag events are being intercepted, <code>false</code> otherwise. | | [isDragInterceptionEnabled()](./puppeteer.page.isdraginterceptionenabled.md) | | <code>true</code> if drag events are being intercepted, <code>false</code> otherwise. |
| [isJavaScriptEnabled()](./puppeteer.page.isjavascriptenabled.md) | | <code>true</code> if the page has JavaScript enabled, <code>false</code> otherwise. | | [isJavaScriptEnabled()](./puppeteer.page.isjavascriptenabled.md) | | <code>true</code> if the page has JavaScript enabled, <code>false</code> otherwise. |
| [isServiceWorkerBypassed()](./puppeteer.page.isserviceworkerbypassed.md) | | <code>true</code> if the service worker are being bypassed, <code>false</code> otherwise. | | [isServiceWorkerBypassed()](./puppeteer.page.isserviceworkerbypassed.md) | | <code>true</code> if the service worker are being bypassed, <code>false</code> otherwise. |
| [locator(selector)](./puppeteer.page.locator.md) | | Creates a locator for the provided <code>selector</code>. See [Locator](./puppeteer.locator.md) for details and supported actions. | | [locator(selector)](./puppeteer.page.locator.md) | | Creates a locator for the provided selector. See [Locator](./puppeteer.locator.md) for details and supported actions. |
| [locator(func)](./puppeteer.page.locator_1.md) | | Creates a locator for the provided function. See [Locator](./puppeteer.locator.md) for details and supported actions. |
| [mainFrame()](./puppeteer.page.mainframe.md) | | The page's main frame. | | [mainFrame()](./puppeteer.page.mainframe.md) | | The page's main frame. |
| [metrics()](./puppeteer.page.metrics.md) | | Object containing metrics as key/value pairs. | | [metrics()](./puppeteer.page.metrics.md) | | Object containing metrics as key/value pairs. |
| [off(eventName, handler)](./puppeteer.page.off.md) | | | | [off(eventName, handler)](./puppeteer.page.off.md) | | |

View File

@ -1,9 +1,9 @@
# Locators # Locators
Locators is a new experimental API that combines `waitForSelector` and element Locators is a new, experimental API that combines the functionalities of
actions in a single unit. In combination with additional precondition checks waiting and actions. With additional precondition checks, it
this allows locators to retry failed actions automatically leading to less flaky enables automatic retries for failed actions, resulting in more reliable and
automation scripts. less flaky automation scripts.
:::note :::note
@ -12,7 +12,42 @@ in the Locators API.
::: :::
## Clicking an element ## Use cases
### Waiting for an element
```ts
await page.locator('button').wait();
```
The following preconditions are automatically checked:
- Waits for the element to become
[visible](https://pptr.dev/api/puppeteer.elementhandle.isvisible/) or hidden.
### Waiting for a function
```ts
await page
.locator(() => {
let resolve!: (node: HTMLCanvasElement) => void;
const promise = new Promise(res => {
return (resolve = res);
});
const observer = new MutationObserver(records => {
for (const record of records) {
if (record.target instanceof HTMLCanvasElement) {
resolve(record.target);
}
}
});
observer.observe(document);
return promise;
})
.wait();
```
### Clicking an element
```ts ```ts
await page.locator('button').click(); await page.locator('button').click();
@ -27,7 +62,25 @@ The following preconditions are automatically checked:
- Waits for the element to have a stable bounding box over two consecutive - Waits for the element to have a stable bounding box over two consecutive
animation frames. animation frames.
## Filling out an input ### Clicking an element matching a criteria
```ts
await page
.locator('button')
.filter(button => !button.disabled)
.click();
```
The following preconditions are automatically checked:
- Ensures the element is in the viewport.
- Waits for the element to become
[visible](https://pptr.dev/api/puppeteer.elementhandle.isvisible/) or hidden.
- Waits for the element to become enabled.
- Waits for the element to have a stable bounding box over two consecutive
animation frames.
### Filling out an input
```ts ```ts
await page.locator('input').fill('value'); await page.locator('input').fill('value');
@ -44,7 +97,16 @@ The following preconditions are automatically checked:
- Waits for the element to have a stable bounding box over two consecutive - Waits for the element to have a stable bounding box over two consecutive
animation frames. animation frames.
## Hover over an element ### Retrieving an element property
```ts
const enabled = await page
.locator('button')
.map(button => !button.disabled)
.wait();
```
### Hover over an element
```ts ```ts
await page.locator('div').hover(); await page.locator('div').hover();
@ -58,7 +120,7 @@ The following preconditions are automatically checked:
- Waits for the element to have a stable bounding box over two consecutive - Waits for the element to have a stable bounding box over two consecutive
animation frames. animation frames.
## Scroll an element ### Scroll an element
```ts ```ts
await page.locator('div').scroll({ await page.locator('div').scroll({

View File

@ -28,6 +28,7 @@ import {
import {LazyArg} from '../common/LazyArg.js'; import {LazyArg} from '../common/LazyArg.js';
import {PuppeteerLifeCycleEvent} from '../common/LifecycleWatcher.js'; import {PuppeteerLifeCycleEvent} from '../common/LifecycleWatcher.js';
import { import {
Awaitable,
EvaluateFunc, EvaluateFunc,
EvaluateFuncWith, EvaluateFuncWith,
HandleFor, HandleFor,
@ -39,7 +40,7 @@ import {TaskManager} from '../common/WaitTask.js';
import {KeyboardTypeOptions} from './Input.js'; import {KeyboardTypeOptions} from './Input.js';
import {JSHandle} from './JSHandle.js'; import {JSHandle} from './JSHandle.js';
import {Locator, NodeLocator} from './locators/locators.js'; import {Locator, FunctionLocator, NodeLocator} from './locators/locators.js';
/** /**
* @internal * @internal
@ -417,7 +418,7 @@ export class Frame {
} }
/** /**
* Creates a locator for the provided `selector`. See {@link Locator} for * Creates a locator for the provided selector. See {@link Locator} for
* details and supported actions. * details and supported actions.
* *
* @remarks * @remarks
@ -426,10 +427,26 @@ export class Frame {
*/ */
locator<Selector extends string>( locator<Selector extends string>(
selector: Selector selector: Selector
): Locator<NodeFor<Selector>> { ): Locator<NodeFor<Selector>>;
return NodeLocator.create(this, selector);
}
/**
* Creates a locator for the provided function. See {@link Locator} for
* details and supported actions.
*
* @remarks
* Locators API is experimental and we will not follow semver for breaking
* change in the Locators API.
*/
locator<Ret>(func: () => Awaitable<Ret>): Locator<Ret>;
locator<Selector extends string, Ret>(
selectorOrFunc: Selector | (() => Awaitable<Ret>)
): Locator<NodeFor<Selector>> | Locator<Ret> {
if (typeof selectorOrFunc === 'string') {
return NodeLocator.create(this, selectorOrFunc);
} else {
return FunctionLocator.create(this, selectorOrFunc);
}
}
/** /**
* Queries the frame for an element matching the given selector. * Queries the frame for an element matching the given selector.
* *

View File

@ -46,6 +46,7 @@ import {
import type {Viewport} from '../common/PuppeteerViewport.js'; import type {Viewport} from '../common/PuppeteerViewport.js';
import type {Tracing} from '../common/Tracing.js'; import type {Tracing} from '../common/Tracing.js';
import type { import type {
Awaitable,
EvaluateFunc, EvaluateFunc,
EvaluateFuncWith, EvaluateFuncWith,
HandleFor, HandleFor,
@ -73,7 +74,12 @@ import type {
} from './Frame.js'; } from './Frame.js';
import {Keyboard, KeyboardTypeOptions, Mouse, Touchscreen} from './Input.js'; import {Keyboard, KeyboardTypeOptions, Mouse, Touchscreen} from './Input.js';
import type {JSHandle} from './JSHandle.js'; import type {JSHandle} from './JSHandle.js';
import {AwaitedLocator, Locator, NodeLocator} from './locators/locators.js'; import {
AwaitedLocator,
FunctionLocator,
Locator,
NodeLocator,
} from './locators/locators.js';
import type {Target} from './Target.js'; import type {Target} from './Target.js';
/** /**
@ -832,7 +838,7 @@ export class Page extends EventEmitter {
} }
/** /**
* Creates a locator for the provided `selector`. See {@link Locator} for * Creates a locator for the provided selector. See {@link Locator} for
* details and supported actions. * details and supported actions.
* *
* @remarks * @remarks
@ -841,8 +847,25 @@ export class Page extends EventEmitter {
*/ */
locator<Selector extends string>( locator<Selector extends string>(
selector: Selector selector: Selector
): Locator<NodeFor<Selector>> { ): Locator<NodeFor<Selector>>;
return NodeLocator.create(this, selector);
/**
* Creates a locator for the provided function. See {@link Locator} for
* details and supported actions.
*
* @remarks
* Locators API is experimental and we will not follow semver for breaking
* change in the Locators API.
*/
locator<Ret>(func: () => Awaitable<Ret>): Locator<Ret>;
locator<Selector extends string, Ret>(
selectorOrFunc: Selector | (() => Awaitable<Ret>)
): Locator<NodeFor<Selector>> | Locator<Ret> {
if (typeof selectorOrFunc === 'string') {
return NodeLocator.create(this, selectorOrFunc);
} else {
return FunctionLocator.create(this, selectorOrFunc);
}
} }
/** /**

View File

@ -0,0 +1,69 @@
/**
* Copyright 2023 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
Observable,
defer,
from,
throwIfEmpty,
} from '../../../third_party/rxjs/rxjs.js';
import {Awaitable, HandleFor} from '../../common/types.js';
import {Frame} from '../Frame.js';
import {Page} from '../Page.js';
import {ActionOptions, Locator} from './locators.js';
/**
* @internal
*/
export class FunctionLocator<T> extends Locator<T> {
static create<Ret>(
pageOrFrame: Page | Frame,
func: () => Awaitable<Ret>
): Locator<Ret> {
return new FunctionLocator<Ret>(pageOrFrame, func).setTimeout(
'getDefaultTimeout' in pageOrFrame
? pageOrFrame.getDefaultTimeout()
: pageOrFrame.page().getDefaultTimeout()
);
}
#pageOrFrame: Page | Frame;
#func: () => Awaitable<T>;
private constructor(pageOrFrame: Page | Frame, func: () => Awaitable<T>) {
super();
this.#pageOrFrame = pageOrFrame;
this.#func = func;
}
override _clone(): FunctionLocator<T> {
return new FunctionLocator(this.#pageOrFrame, this.#func);
}
_wait(options?: Readonly<ActionOptions>): Observable<HandleFor<T>> {
const signal = options?.signal;
return defer(() => {
return from(
this.#pageOrFrame.waitForFunction(this.#func, {
timeout: this.timeout,
signal,
})
);
}).pipe(throwIfEmpty());
}
}

View File

@ -146,10 +146,10 @@ export interface LocatorEventObject {
} }
/** /**
* Locators describe a strategy of locating elements and performing an action on * Locators describe a strategy of locating objects and performing an action on
* them. If the action fails because the element is not ready for the action, * them. If the action fails because the object is not ready for the action, the
* the whole operation is retried. Various preconditions for a successful action * whole operation is retried. Various preconditions for a successful action are
* are checked automatically. * checked automatically.
* *
* @public * @public
*/ */

View File

@ -20,3 +20,4 @@ export * from './FilteredLocator.js';
export * from './RaceLocator.js'; export * from './RaceLocator.js';
export * from './DelegatedLocator.js'; export * from './DelegatedLocator.js';
export * from './MappedLocator.js'; export * from './MappedLocator.js';
export * from './FunctionLocator.js';

View File

@ -672,4 +672,34 @@ describe('Locator', function () {
} }
}); });
}); });
describe('FunctionLocator', () => {
it('should work', async () => {
const {page} = await getTestState();
const result = page
.locator(() => {
return new Promise<boolean>(resolve => {
return setTimeout(() => {
return resolve(true);
}, 100);
});
})
.wait();
await expect(result).resolves.toEqual(true);
});
it('should work with actions', async () => {
const {page} = await getTestState();
await page.setContent(`<div onclick="window.clicked = true">test</div>`);
await page
.locator(() => {
return document.getElementsByTagName('div')[0] as HTMLDivElement;
})
.click();
await expect(
page.evaluate(() => {
return (window as unknown as {clicked: boolean}).clicked;
})
).resolves.toEqual(true);
});
});
}); });