mirror of
https://github.com/puppeteer/puppeteer
synced 2024-06-14 14:02:48 +00:00
chore: add methods to configure locators (#10273)
This commit is contained in:
parent
b03acac30f
commit
54d6192262
@ -22,18 +22,42 @@ import {isErrorLike} from '../util/ErrorLike.js';
|
||||
import {ElementHandle, BoundingBox, ClickOptions} from './ElementHandle.js';
|
||||
import type {Page} from './Page.js';
|
||||
|
||||
type VisibilityOption = 'hidden' | 'visible' | null;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export interface LocatorOptions {
|
||||
/**
|
||||
* Whether to wait for the element to be `visible` or `hidden`.
|
||||
* Whether to wait for the element to be `visible` or `hidden`. `null` to
|
||||
* disable visibility checks.
|
||||
*/
|
||||
visibility: 'hidden' | 'visible';
|
||||
visibility: VisibilityOption;
|
||||
/**
|
||||
* Total timeout for the entire locator operation.
|
||||
*
|
||||
* Pass `0` to disable timeout.
|
||||
*
|
||||
* @defaultValue `Page.getDefaultTimeout()`
|
||||
*/
|
||||
timeout: number;
|
||||
/**
|
||||
* Whether to scroll the element into viewport if not in the viewprot already.
|
||||
* @defaultValue `true`
|
||||
*/
|
||||
ensureElementIsInTheViewport: boolean;
|
||||
/**
|
||||
* Whether to wait for input elements to become enabled before the action.
|
||||
* Applicable to `click` and `fill` actions.
|
||||
* @defaultValue `true`
|
||||
*/
|
||||
waitForEnabled: boolean;
|
||||
/**
|
||||
* Whether to wait for the element's bounding box to be same between two
|
||||
* animation frames.
|
||||
* @defaultValue `true`
|
||||
*/
|
||||
waitForStableBoundingBox: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -98,6 +122,9 @@ export class Locator extends EventEmitter {
|
||||
options: LocatorOptions = {
|
||||
visibility: 'visible',
|
||||
timeout: page.getDefaultTimeout(),
|
||||
ensureElementIsInTheViewport: true,
|
||||
waitForEnabled: true,
|
||||
waitForStableBoundingBox: true,
|
||||
}
|
||||
) {
|
||||
super();
|
||||
@ -127,6 +154,31 @@ export class Locator extends EventEmitter {
|
||||
return super.off(eventName, handler);
|
||||
}
|
||||
|
||||
setVisibility(visibility: VisibilityOption): this {
|
||||
this.#options.visibility = visibility;
|
||||
return this;
|
||||
}
|
||||
|
||||
setTimeout(timeout: number): this {
|
||||
this.#options.timeout = timeout;
|
||||
return this;
|
||||
}
|
||||
|
||||
setEnsureElementIsInTheViewport(value: boolean): this {
|
||||
this.#options.ensureElementIsInTheViewport = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
setWaitForEnabled(value: boolean): this {
|
||||
this.#options.waitForEnabled = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
setWaitForStableBoundingBox(value: boolean): this {
|
||||
this.#options.waitForStableBoundingBox = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retries the `fn` until a truthy result is returned.
|
||||
*/
|
||||
@ -138,10 +190,12 @@ export class Locator extends EventEmitter {
|
||||
let isActive = true;
|
||||
let controller: AbortController;
|
||||
// If the loop times out, we abort only the last iteration's controller.
|
||||
const timeoutId = setTimeout(() => {
|
||||
const timeoutId = timeout
|
||||
? setTimeout(() => {
|
||||
isActive = false;
|
||||
controller?.abort();
|
||||
}, timeout);
|
||||
}, timeout)
|
||||
: 0;
|
||||
// If the user's signal aborts, we abort the last iteration and the loop.
|
||||
signal?.addEventListener(
|
||||
'abort',
|
||||
@ -194,6 +248,9 @@ export class Locator extends EventEmitter {
|
||||
element: ElementHandle,
|
||||
signal?: AbortSignal
|
||||
): Promise<void> => {
|
||||
if (!this.#options.ensureElementIsInTheViewport) {
|
||||
return;
|
||||
}
|
||||
// Side-effect: this also checks if it is connected.
|
||||
const isIntersectingViewport = await element.isIntersectingViewport({
|
||||
threshold: 0,
|
||||
@ -221,6 +278,9 @@ export class Locator extends EventEmitter {
|
||||
element: ElementHandle,
|
||||
signal?: AbortSignal
|
||||
): Promise<void> => {
|
||||
if (this.#options.visibility === null) {
|
||||
return;
|
||||
}
|
||||
if (this.#options.visibility === 'hidden') {
|
||||
await this.#waitForFunction(async () => {
|
||||
return element.isHidden();
|
||||
@ -239,6 +299,9 @@ export class Locator extends EventEmitter {
|
||||
element: ElementHandle,
|
||||
signal?: AbortSignal
|
||||
): Promise<void> => {
|
||||
if (!this.#options.waitForEnabled) {
|
||||
return;
|
||||
}
|
||||
await this.#page.waitForFunction(
|
||||
el => {
|
||||
if (['button', 'textarea', 'input', 'select'].includes(el.tagName)) {
|
||||
@ -262,6 +325,9 @@ export class Locator extends EventEmitter {
|
||||
element: ElementHandle,
|
||||
signal?: AbortSignal
|
||||
): Promise<void> => {
|
||||
if (!this.#options.waitForStableBoundingBox) {
|
||||
return;
|
||||
}
|
||||
function getClientRect() {
|
||||
return element.evaluate(el => {
|
||||
return new Promise<[BoundingBox, BoundingBox]>(resolve => {
|
||||
|
@ -29,6 +29,33 @@ describe('Locator', function () {
|
||||
setupTestBrowserHooks();
|
||||
setupTestPageAndContextHooks();
|
||||
|
||||
it('should work without preconditions', async () => {
|
||||
const {page} = getTestState();
|
||||
|
||||
await page.setViewport({width: 500, height: 500});
|
||||
await page.setContent(`
|
||||
<button onclick="this.innerText = 'clicked';">test</button>
|
||||
`);
|
||||
let willClick = false;
|
||||
await page
|
||||
.locator('button')
|
||||
.setEnsureElementIsInTheViewport(false)
|
||||
.setTimeout(0)
|
||||
.setVisibility(null)
|
||||
.setWaitForEnabled(false)
|
||||
.setWaitForStableBoundingBox(false)
|
||||
.on(LocatorEmittedEvents.Action, () => {
|
||||
willClick = true;
|
||||
})
|
||||
.click();
|
||||
const button = await page.$('button');
|
||||
const text = await button?.evaluate(el => {
|
||||
return el.innerText;
|
||||
});
|
||||
expect(text).toBe('clicked');
|
||||
expect(willClick).toBe(true);
|
||||
});
|
||||
|
||||
describe('Locator.click', function () {
|
||||
it('should work', async () => {
|
||||
const {page} = getTestState();
|
||||
|
Loading…
Reference in New Issue
Block a user