chore: implement waitForFrame and use clickablePoint for ElementHandle operations (#10778)

This commit is contained in:
jrandolf 2023-08-24 20:32:29 +02:00 committed by GitHub
parent a4a2cf1d39
commit c4a4412920
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 245 additions and 330 deletions

View File

@ -10,9 +10,9 @@ This method scrolls element into view if needed, and then uses [Page.mouse](./pu
```typescript ```typescript
class ElementHandle { class ElementHandle {
abstract click( click(
this: ElementHandle<Element>, this: ElementHandle<Element>,
options?: ClickOptions options?: Readonly<ClickOptions>
): Promise<void>; ): Promise<void>;
} }
``` ```
@ -22,7 +22,7 @@ class ElementHandle {
| Parameter | Type | Description | | Parameter | Type | Description |
| --------- | ------------------------------------------------------------ | ------------ | | --------- | ------------------------------------------------------------ | ------------ |
| this | [ElementHandle](./puppeteer.elementhandle.md)&lt;Element&gt; | | | this | [ElementHandle](./puppeteer.elementhandle.md)&lt;Element&gt; | |
| options | [ClickOptions](./puppeteer.clickoptions.md) | _(Optional)_ | | options | Readonly&lt;[ClickOptions](./puppeteer.clickoptions.md)&gt; | _(Optional)_ |
**Returns:** **Returns:**

View File

@ -10,7 +10,7 @@ This method scrolls element into view if needed, and then uses [Page](./puppetee
```typescript ```typescript
class ElementHandle { class ElementHandle {
abstract hover(this: ElementHandle<Element>): Promise<void>; hover(this: ElementHandle<Element>): Promise<void>;
} }
``` ```

View File

@ -10,10 +10,7 @@ Focuses the element, and then uses [Keyboard.down()](./puppeteer.keyboard.down.m
```typescript ```typescript
class ElementHandle { class ElementHandle {
abstract press( press(key: KeyInput, options?: Readonly<KeyPressOptions>): Promise<void>;
key: KeyInput,
options?: Readonly<KeyPressOptions>
): Promise<void>;
} }
``` ```

View File

@ -10,7 +10,7 @@ This method scrolls element into view if needed, and then uses [Touchscreen.tap(
```typescript ```typescript
class ElementHandle { class ElementHandle {
abstract tap(this: ElementHandle<Element>): Promise<void>; tap(this: ElementHandle<Element>): Promise<void>;
} }
``` ```

View File

@ -8,7 +8,7 @@ sidebar_label: ElementHandle.touchEnd
```typescript ```typescript
class ElementHandle { class ElementHandle {
abstract touchEnd(this: ElementHandle<Element>): Promise<void>; touchEnd(this: ElementHandle<Element>): Promise<void>;
} }
``` ```

View File

@ -8,7 +8,7 @@ sidebar_label: ElementHandle.touchMove
```typescript ```typescript
class ElementHandle { class ElementHandle {
abstract touchMove(this: ElementHandle<Element>): Promise<void>; touchMove(this: ElementHandle<Element>): Promise<void>;
} }
``` ```

View File

@ -8,7 +8,7 @@ sidebar_label: ElementHandle.touchStart
```typescript ```typescript
class ElementHandle { class ElementHandle {
abstract touchStart(this: ElementHandle<Element>): Promise<void>; touchStart(this: ElementHandle<Element>): Promise<void>;
} }
``` ```

View File

@ -12,10 +12,7 @@ To press a special key, like `Control` or `ArrowDown`, use [ElementHandle.press(
```typescript ```typescript
class ElementHandle { class ElementHandle {
abstract type( type(text: string, options?: Readonly<KeyboardTypeOptions>): Promise<void>;
text: string,
options?: Readonly<KeyboardTypeOptions>
): Promise<void>;
} }
``` ```

View File

@ -157,7 +157,7 @@ page.off('request', logRequest);
| [viewport()](./puppeteer.page.viewport.md) | | Current page viewport settings. | | [viewport()](./puppeteer.page.viewport.md) | | Current page viewport settings. |
| [waitForDevicePrompt(options)](./puppeteer.page.waitfordeviceprompt.md) | | <p>This method is typically coupled with an action that triggers a device request from an api such as WebBluetooth.</p><p>:::caution</p><p>This must be called before the device request is made. It will not return a currently active device prompt.</p><p>:::</p> | | [waitForDevicePrompt(options)](./puppeteer.page.waitfordeviceprompt.md) | | <p>This method is typically coupled with an action that triggers a device request from an api such as WebBluetooth.</p><p>:::caution</p><p>This must be called before the device request is made. It will not return a currently active device prompt.</p><p>:::</p> |
| [waitForFileChooser(options)](./puppeteer.page.waitforfilechooser.md) | | <p>This method is typically coupled with an action that triggers file choosing.</p><p>:::caution</p><p>This must be called before the file chooser is launched. It will not return a currently active file chooser.</p><p>:::</p> | | [waitForFileChooser(options)](./puppeteer.page.waitforfilechooser.md) | | <p>This method is typically coupled with an action that triggers file choosing.</p><p>:::caution</p><p>This must be called before the file chooser is launched. It will not return a currently active file chooser.</p><p>:::</p> |
| [waitForFrame(urlOrPredicate, options)](./puppeteer.page.waitforframe.md) | | | | [waitForFrame(urlOrPredicate, options)](./puppeteer.page.waitforframe.md) | | Waits for a frame matching the given conditions to appear. |
| [waitForFunction(pageFunction, options, args)](./puppeteer.page.waitforfunction.md) | | Waits for a function to finish evaluating in the page's context. | | [waitForFunction(pageFunction, options, args)](./puppeteer.page.waitforfunction.md) | | Waits for a function to finish evaluating in the page's context. |
| [waitForNavigation(options)](./puppeteer.page.waitfornavigation.md) | | Waits for the page to navigate to a new URL or to reload. It is useful when you run code that will indirectly cause the page to navigate. | | [waitForNavigation(options)](./puppeteer.page.waitfornavigation.md) | | Waits for the page to navigate to a new URL or to reload. It is useful when you run code that will indirectly cause the page to navigate. |
| [waitForNetworkIdle(options)](./puppeteer.page.waitfornetworkidle.md) | | | | [waitForNetworkIdle(options)](./puppeteer.page.waitfornetworkidle.md) | | |

View File

@ -4,15 +4,15 @@ sidebar_label: Page.waitForFrame
# Page.waitForFrame() method # Page.waitForFrame() method
Waits for a frame matching the given conditions to appear.
#### Signature: #### Signature:
```typescript ```typescript
class Page { class Page {
waitForFrame( waitForFrame(
urlOrPredicate: string | ((frame: Frame) => boolean | Promise<boolean>), urlOrPredicate: string | ((frame: Frame) => Awaitable<boolean>),
options?: { options?: WaitTimeoutOptions
timeout?: number;
}
): Promise<Frame>; ): Promise<Frame>;
} }
``` ```
@ -20,22 +20,14 @@ class Page {
## Parameters ## Parameters
| Parameter | Type | Description | | Parameter | Type | Description |
| -------------- | ------------------------------------------------------------------------------------------ | ---------------------------------------- | | -------------- | ------------------------------------------------------------------------------------------------------------- | ------------ |
| urlOrPredicate | string \| ((frame: [Frame](./puppeteer.frame.md)) =&gt; boolean \| Promise&lt;boolean&gt;) | A URL or predicate to wait for. | | urlOrPredicate | string \| ((frame: [Frame](./puppeteer.frame.md)) =&gt; [Awaitable](./puppeteer.awaitable.md)&lt;boolean&gt;) | |
| options | { timeout?: number; } | _(Optional)_ Optional waiting parameters | | options | [WaitTimeoutOptions](./puppeteer.waittimeoutoptions.md) | _(Optional)_ |
**Returns:** **Returns:**
Promise&lt;[Frame](./puppeteer.frame.md)&gt; Promise&lt;[Frame](./puppeteer.frame.md)&gt;
Promise which resolves to the matched frame.
## Remarks
Optional Parameter have:
- `timeout`: Maximum wait time in milliseconds, defaults to `30` seconds, pass `0` to disable the timeout. The default value can be changed by using the [Page.setDefaultTimeout()](./puppeteer.page.setdefaulttimeout.md) method.
## Example ## Example
```ts ```ts

View File

@ -652,17 +652,25 @@ export abstract class ElementHandle<
* uses {@link Page} to hover over the center of the element. * uses {@link Page} to hover over the center of the element.
* If the element is detached from DOM, the method throws an error. * If the element is detached from DOM, the method throws an error.
*/ */
abstract hover(this: ElementHandle<Element>): Promise<void>; async hover(this: ElementHandle<Element>): Promise<void> {
await this.scrollIntoViewIfNeeded();
const {x, y} = await this.clickablePoint();
await this.frame.page().mouse.move(x, y);
}
/** /**
* This method scrolls element into view if needed, and then * This method scrolls element into view if needed, and then
* uses {@link Page | Page.mouse} to click in the center of the element. * uses {@link Page | Page.mouse} to click in the center of the element.
* If the element is detached from DOM, the method throws an error. * If the element is detached from DOM, the method throws an error.
*/ */
abstract click( async click(
this: ElementHandle<Element>, this: ElementHandle<Element>,
options?: ClickOptions options: Readonly<ClickOptions> = {}
): Promise<void>; ): Promise<void> {
await this.scrollIntoViewIfNeeded();
const {x, y} = await this.clickablePoint(options.offset);
await this.frame.page().mouse.click(x, y, options);
}
/** /**
* This method creates and captures a dragevent from the element. * This method creates and captures a dragevent from the element.
@ -804,13 +812,29 @@ export abstract class ElementHandle<
* {@link Touchscreen.tap} to tap in the center of the element. * {@link Touchscreen.tap} to tap in the center of the element.
* If the element is detached from DOM, the method throws an error. * If the element is detached from DOM, the method throws an error.
*/ */
abstract tap(this: ElementHandle<Element>): Promise<void>; async tap(this: ElementHandle<Element>): Promise<void> {
await this.scrollIntoViewIfNeeded();
const {x, y} = await this.clickablePoint();
await this.frame.page().touchscreen.touchStart(x, y);
await this.frame.page().touchscreen.touchEnd();
}
abstract touchStart(this: ElementHandle<Element>): Promise<void>; async touchStart(this: ElementHandle<Element>): Promise<void> {
await this.scrollIntoViewIfNeeded();
const {x, y} = await this.clickablePoint();
await this.frame.page().touchscreen.touchStart(x, y);
}
abstract touchMove(this: ElementHandle<Element>): Promise<void>; async touchMove(this: ElementHandle<Element>): Promise<void> {
await this.scrollIntoViewIfNeeded();
const {x, y} = await this.clickablePoint();
await this.frame.page().touchscreen.touchMove(x, y);
}
abstract touchEnd(this: ElementHandle<Element>): Promise<void>; async touchEnd(this: ElementHandle<Element>): Promise<void> {
await this.scrollIntoViewIfNeeded();
await this.frame.page().touchscreen.touchEnd();
}
/** /**
* Calls {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus | focus} on the element. * Calls {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus | focus} on the element.
@ -849,10 +873,13 @@ export abstract class ElementHandle<
* *
* @param options - Delay in milliseconds. Defaults to 0. * @param options - Delay in milliseconds. Defaults to 0.
*/ */
abstract type( async type(
text: string, text: string,
options?: Readonly<KeyboardTypeOptions> options?: Readonly<KeyboardTypeOptions>
): Promise<void>; ): Promise<void> {
await this.focus();
await this.frame.page().keyboard.type(text, options);
}
/** /**
* Focuses the element, and then uses {@link Keyboard.down} and {@link Keyboard.up}. * Focuses the element, and then uses {@link Keyboard.down} and {@link Keyboard.up}.
@ -868,26 +895,29 @@ export abstract class ElementHandle<
* @param key - Name of key to press, such as `ArrowLeft`. * @param key - Name of key to press, such as `ArrowLeft`.
* See {@link KeyInput} for a list of all key names. * See {@link KeyInput} for a list of all key names.
*/ */
abstract press( async press(
key: KeyInput, key: KeyInput,
options?: Readonly<KeyPressOptions> options?: Readonly<KeyPressOptions>
): Promise<void>; ): Promise<void> {
await this.focus();
await this.frame.page().keyboard.press(key, options);
}
async #clickableBox(): Promise<BoundingBox | null> { async #clickableBox(): Promise<BoundingBox | null> {
const adoptedThis = await this.frame.isolatedRealm().adoptHandle(this); const adoptedThis = await this.frame.isolatedRealm().adoptHandle(this);
const rects = await adoptedThis.evaluate(element => { const boxes = await adoptedThis.evaluate(element => {
if (!(element instanceof Element)) { if (!(element instanceof Element)) {
return null; return null;
} }
return [...element.getClientRects()].map(rect => { return [...element.getClientRects()].map(rect => {
return rect.toJSON(); return {x: rect.x, y: rect.y, width: rect.width, height: rect.height};
}) as DOMRect[]; });
}); });
void adoptedThis.dispose().catch(debugError); void adoptedThis.dispose().catch(debugError);
if (!rects?.length) { if (!boxes?.length) {
return null; return null;
} }
await this.#intersectBoundingBoxesWithFrame(rects); await this.#intersectBoundingBoxesWithFrame(boxes);
let frame: Frame | null | undefined = this.frame; let frame: Frame | null | undefined = this.frame;
let element: HandleFor<HTMLIFrameElement> | null | undefined; let element: HandleFor<HTMLIFrameElement> | null | undefined;
while ((element = await frame?.frameElement())) { while ((element = await frame?.frameElement())) {
@ -914,27 +944,27 @@ export abstract class ElementHandle<
if (!parentBox) { if (!parentBox) {
return null; return null;
} }
for (const box of rects) { for (const box of boxes) {
box.x += parentBox.left; box.x += parentBox.left;
box.y += parentBox.top; box.y += parentBox.top;
} }
await element.#intersectBoundingBoxesWithFrame(rects); await element.#intersectBoundingBoxesWithFrame(boxes);
frame = frame?.parentFrame(); frame = frame?.parentFrame();
} finally { } finally {
void element.dispose().catch(debugError); void element.dispose().catch(debugError);
} }
} }
const rect = rects.find(box => { const box = boxes.find(box => {
return box.width >= 1 && box.height >= 1; return box.width >= 1 && box.height >= 1;
}); });
if (!rect) { if (!box) {
return null; return null;
} }
return { return {
x: rect.x, x: box.x,
y: rect.y, y: box.y,
height: rect.height, height: box.height,
width: rect.width, width: box.width,
}; };
} }
@ -967,7 +997,7 @@ export abstract class ElementHandle<
return null; return null;
} }
const rect = element.getBoundingClientRect(); const rect = element.getBoundingClientRect();
return rect.toJSON() as DOMRect; return {x: rect.x, y: rect.y, width: rect.width, height: rect.height};
}); });
void adoptedThis.dispose().catch(debugError); void adoptedThis.dispose().catch(debugError);
if (!box) { if (!box) {
@ -977,11 +1007,9 @@ export abstract class ElementHandle<
if (!offset) { if (!offset) {
return null; return null;
} }
box.x += offset.x;
box.y += offset.y;
return { return {
x: box.x, x: box.x + offset.x,
y: box.y, y: box.y + offset.y,
height: box.height, height: box.height,
width: box.width, width: box.width,
}; };

View File

@ -18,6 +18,18 @@ import type {Readable} from 'stream';
import {Protocol} from 'devtools-protocol'; import {Protocol} from 'devtools-protocol';
import {
filterAsync,
first,
firstValueFrom,
from,
fromEvent,
map,
merge,
Observable,
raceWith,
timer,
} from '../../third_party/rxjs/rxjs.js';
import type {HTTPRequest} from '../api/HTTPRequest.js'; import type {HTTPRequest} from '../api/HTTPRequest.js';
import type {HTTPResponse} from '../api/HTTPResponse.js'; import type {HTTPResponse} from '../api/HTTPResponse.js';
import type {Accessibility} from '../common/Accessibility.js'; import type {Accessibility} from '../common/Accessibility.js';
@ -26,7 +38,7 @@ import type {ConsoleMessage} from '../common/ConsoleMessage.js';
import type {Coverage} from '../common/Coverage.js'; import type {Coverage} from '../common/Coverage.js';
import {Device} from '../common/Device.js'; import {Device} from '../common/Device.js';
import {DeviceRequestPrompt} from '../common/DeviceRequestPrompt.js'; import {DeviceRequestPrompt} from '../common/DeviceRequestPrompt.js';
import {TargetCloseError} from '../common/Errors.js'; import {TargetCloseError, TimeoutError} from '../common/Errors.js';
import {EventEmitter, Handler} from '../common/EventEmitter.js'; import {EventEmitter, Handler} from '../common/EventEmitter.js';
import type {FileChooser} from '../common/FileChooser.js'; import type {FileChooser} from '../common/FileChooser.js';
import type {WaitForSelectorOptions} from '../common/IsolatedWorld.js'; import type {WaitForSelectorOptions} from '../common/IsolatedWorld.js';
@ -1745,9 +1757,8 @@ export class Page extends EventEmitter {
} }
/** /**
* @param urlOrPredicate - A URL or predicate to wait for. * Waits for a frame matching the given conditions to appear.
* @param options - Optional waiting parameters *
* @returns Promise which resolves to the matched frame.
* @example * @example
* *
* ```ts * ```ts
@ -1755,20 +1766,41 @@ export class Page extends EventEmitter {
* return frame.name() === 'Test'; * return frame.name() === 'Test';
* }); * });
* ``` * ```
*
* @remarks
* Optional Parameter have:
*
* - `timeout`: Maximum wait time in milliseconds, defaults to `30` seconds,
* pass `0` to disable the timeout. The default value can be changed by using
* the {@link Page.setDefaultTimeout} method.
*/ */
async waitForFrame( async waitForFrame(
urlOrPredicate: string | ((frame: Frame) => boolean | Promise<boolean>), urlOrPredicate: string | ((frame: Frame) => Awaitable<boolean>),
options?: {timeout?: number} options: WaitTimeoutOptions = {}
): Promise<Frame>; ): Promise<Frame> {
async waitForFrame(): Promise<Frame> { const {timeout: ms = this.getDefaultTimeout()} = options;
throw new Error('Not implemented');
if (isString(urlOrPredicate)) {
urlOrPredicate = (frame: Frame) => {
return urlOrPredicate === frame.url();
};
}
return firstValueFrom(
merge(
fromEvent(this, PageEmittedEvents.FrameAttached) as Observable<Frame>,
fromEvent(this, PageEmittedEvents.FrameNavigated) as Observable<Frame>,
from(this.frames())
).pipe(
filterAsync(urlOrPredicate),
first(),
raceWith(
timer(ms === 0 ? Infinity : ms).pipe(
map(() => {
throw new TimeoutError(`Timed out after waiting ${ms}ms`);
})
),
fromEvent(this, PageEmittedEvents.Close).pipe(
map(() => {
throw new TargetCloseError('Page closed.');
})
)
)
)
);
} }
/** /**

View File

@ -16,13 +16,7 @@
import {Protocol} from 'devtools-protocol'; import {Protocol} from 'devtools-protocol';
import { import {AutofillData, ElementHandle, Point} from '../api/ElementHandle.js';
AutofillData,
ClickOptions,
ElementHandle,
Point,
} from '../api/ElementHandle.js';
import {KeyboardTypeOptions, KeyPressOptions} from '../api/Input.js';
import {Page, ScreenshotOptions} from '../api/Page.js'; import {Page, ScreenshotOptions} from '../api/Page.js';
import {assert} from '../util/assert.js'; import {assert} from '../util/assert.js';
@ -33,7 +27,6 @@ import {FrameManager} from './FrameManager.js';
import {WaitForSelectorOptions} from './IsolatedWorld.js'; import {WaitForSelectorOptions} from './IsolatedWorld.js';
import {CDPJSHandle} from './JSHandle.js'; import {CDPJSHandle} from './JSHandle.js';
import {NodeFor} from './types.js'; import {NodeFor} from './types.js';
import {KeyInput} from './USKeyboardLayout.js';
import {debugError} from './util.js'; import {debugError} from './util.js';
/** /**
@ -141,31 +134,6 @@ export class CDPElementHandle<
} }
} }
/**
* This method scrolls element into view if needed, and then
* uses {@link Page.mouse} to hover over the center of the element.
* If the element is detached from DOM, the method throws an error.
*/
override async hover(this: CDPElementHandle<Element>): Promise<void> {
await this.scrollIntoViewIfNeeded();
const {x, y} = await this.clickablePoint();
await this.#page.mouse.move(x, y);
}
/**
* This method scrolls element into view if needed, and then
* uses {@link Page.mouse} to click in the center of the element.
* If the element is detached from DOM, the method throws an error.
*/
override async click(
this: CDPElementHandle<Element>,
options: Readonly<ClickOptions> = {}
): Promise<void> {
await this.scrollIntoViewIfNeeded();
const {x, y} = await this.clickablePoint(options.offset);
await this.#page.mouse.click(x, y, options);
}
/** /**
* This method creates and captures a dragevent from the element. * This method creates and captures a dragevent from the element.
*/ */
@ -281,46 +249,6 @@ export class CDPElementHandle<
} }
} }
override async tap(this: CDPElementHandle<Element>): Promise<void> {
await this.scrollIntoViewIfNeeded();
const {x, y} = await this.clickablePoint();
await this.#page.touchscreen.touchStart(x, y);
await this.#page.touchscreen.touchEnd();
}
override async touchStart(this: CDPElementHandle<Element>): Promise<void> {
await this.scrollIntoViewIfNeeded();
const {x, y} = await this.clickablePoint();
await this.#page.touchscreen.touchStart(x, y);
}
override async touchMove(this: CDPElementHandle<Element>): Promise<void> {
await this.scrollIntoViewIfNeeded();
const {x, y} = await this.clickablePoint();
await this.#page.touchscreen.touchMove(x, y);
}
override async touchEnd(this: CDPElementHandle<Element>): Promise<void> {
await this.scrollIntoViewIfNeeded();
await this.#page.touchscreen.touchEnd();
}
override async type(
text: string,
options?: Readonly<KeyboardTypeOptions>
): Promise<void> {
await this.focus();
await this.#page.keyboard.type(text, options);
}
override async press(
key: KeyInput,
options?: Readonly<KeyPressOptions>
): Promise<void> {
await this.focus();
await this.#page.keyboard.press(key, options);
}
override async screenshot( override async screenshot(
this: CDPElementHandle<Element>, this: CDPElementHandle<Element>,
options: ScreenshotOptions = {} options: ScreenshotOptions = {}

View File

@ -997,53 +997,6 @@ export class CDPPage extends Page {
); );
} }
override async waitForFrame(
urlOrPredicate: string | ((frame: Frame) => boolean | Promise<boolean>),
options: {timeout?: number} = {}
): Promise<Frame> {
const {timeout = this.#timeoutSettings.timeout()} = options;
let predicate: (frame: Frame) => Promise<boolean>;
if (isString(urlOrPredicate)) {
predicate = (frame: Frame) => {
return Promise.resolve(urlOrPredicate === frame.url());
};
} else {
predicate = (frame: Frame) => {
const value = urlOrPredicate(frame);
if (typeof value === 'boolean') {
return Promise.resolve(value);
}
return value;
};
}
const eventRace: Promise<Frame> = Deferred.race([
waitForEvent(
this.#frameManager,
FrameManagerEmittedEvents.FrameAttached,
predicate,
timeout,
this.#sessionCloseDeferred.valueOrThrow()
),
waitForEvent(
this.#frameManager,
FrameManagerEmittedEvents.FrameNavigated,
predicate,
timeout,
this.#sessionCloseDeferred.valueOrThrow()
),
...this.frames().map(async frame => {
if (await predicate(frame)) {
return frame;
}
return await eventRace;
}),
]);
return eventRace;
}
override async goBack( override async goBack(
options: WaitForOptions = {} options: WaitForOptions = {}
): Promise<HTTPResponse | null> { ): Promise<HTTPResponse | null> {

View File

@ -19,11 +19,7 @@ import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import { import {
AutofillData, AutofillData,
ElementHandle as BaseElementHandle, ElementHandle as BaseElementHandle,
ClickOptions,
} from '../../api/ElementHandle.js'; } from '../../api/ElementHandle.js';
import {KeyboardTypeOptions, KeyPressOptions} from '../../api/Input.js';
import {assert} from '../../util/assert.js';
import {KeyInput} from '../USKeyboardLayout.js';
import {debugError} from '../util.js'; import {debugError} from '../util.js';
import {Frame} from './Frame.js'; import {Frame} from './Frame.js';
@ -105,96 +101,4 @@ export class ElementHandle<
} }
return null; return null;
} }
// ///////////////////
// // Input methods //
// ///////////////////
override async click(
this: ElementHandle<Element>,
options?: Readonly<ClickOptions>
): Promise<void> {
await this.scrollIntoViewIfNeeded();
const {x = 0, y = 0} = options?.offset ?? {};
const remoteValue = this.remoteValue();
assert('sharedId' in remoteValue);
return this.#frame.page().mouse.click(
x,
y,
Object.assign({}, options, {
origin: {
type: 'element' as const,
element: remoteValue as Bidi.Script.SharedReference,
},
})
);
}
override async hover(this: ElementHandle<Element>): Promise<void> {
await this.scrollIntoViewIfNeeded();
const remoteValue = this.remoteValue();
assert('sharedId' in remoteValue);
return this.#frame.page().mouse.move(0, 0, {
origin: {
type: 'element' as const,
element: remoteValue as Bidi.Script.SharedReference,
},
});
}
override async tap(this: ElementHandle<Element>): Promise<void> {
await this.scrollIntoViewIfNeeded();
const remoteValue = this.remoteValue();
assert('sharedId' in remoteValue);
return this.#frame.page().touchscreen.tap(0, 0, {
origin: {
type: 'element' as const,
element: remoteValue as Bidi.Script.SharedReference,
},
});
}
override async touchStart(this: ElementHandle<Element>): Promise<void> {
await this.scrollIntoViewIfNeeded();
const remoteValue = this.remoteValue();
assert('sharedId' in remoteValue);
return this.#frame.page().touchscreen.touchStart(0, 0, {
origin: {
type: 'element' as const,
element: remoteValue as Bidi.Script.SharedReference,
},
});
}
override async touchMove(this: ElementHandle<Element>): Promise<void> {
await this.scrollIntoViewIfNeeded();
const remoteValue = this.remoteValue();
assert('sharedId' in remoteValue);
return this.#frame.page().touchscreen.touchMove(0, 0, {
origin: {
type: 'element' as const,
element: remoteValue as Bidi.Script.SharedReference,
},
});
}
override async touchEnd(this: ElementHandle<Element>): Promise<void> {
await this.scrollIntoViewIfNeeded();
await this.#frame.page().touchscreen.touchEnd();
}
override async type(
text: string,
options?: Readonly<KeyboardTypeOptions>
): Promise<void> {
await this.focus();
await this.#frame.page().keyboard.type(text, options);
}
override async press(
key: KeyInput,
options?: Readonly<KeyPressOptions>
): Promise<void> {
await this.focus();
await this.#frame.page().keyboard.press(key, options);
}
} }

View File

@ -483,9 +483,10 @@ export class Mouse extends BaseMouse {
y: number, y: number,
options: Readonly<BidiMouseMoveOptions> = {} options: Readonly<BidiMouseMoveOptions> = {}
): Promise<void> { ): Promise<void> {
// https://w3c.github.io/webdriver-bidi/#command-input-performActions:~:text=input.PointerMoveAction%20%3D%20%7B%0A%20%20type%3A%20%22pointerMove%22%2C%0A%20%20x%3A%20js%2Dint%2C
this.#lastMovePoint = { this.#lastMovePoint = {
x, x: Math.round(x),
y, y: Math.round(y),
}; };
await this.#context.connection.send('input.performActions', { await this.#context.connection.send('input.performActions', {
context: this.#context.id, context: this.#context.id,
@ -496,8 +497,7 @@ export class Mouse extends BaseMouse {
actions: [ actions: [
{ {
type: ActionType.PointerMove, type: ActionType.PointerMove,
x, ...this.#lastMovePoint,
y,
duration: (options.steps ?? 0) * 50, duration: (options.steps ?? 0) * 50,
origin: options.origin, origin: options.origin,
}, },
@ -551,8 +551,8 @@ export class Mouse extends BaseMouse {
const actions: Bidi.Input.PointerSourceAction[] = [ const actions: Bidi.Input.PointerSourceAction[] = [
{ {
type: ActionType.PointerMove, type: ActionType.PointerMove,
x, x: Math.round(x),
y, y: Math.round(y),
origin: options.origin, origin: options.origin,
}, },
]; ];
@ -653,8 +653,8 @@ export class Touchscreen extends BaseTouchscreen {
actions: [ actions: [
{ {
type: ActionType.PointerMove, type: ActionType.PointerMove,
x, x: Math.round(x),
y, y: Math.round(y),
origin: options.origin, origin: options.origin,
}, },
{ {
@ -684,8 +684,8 @@ export class Touchscreen extends BaseTouchscreen {
actions: [ actions: [
{ {
type: ActionType.PointerMove, type: ActionType.PointerMove,
x, x: Math.round(x),
y, y: Math.round(y),
origin: options.origin, origin: options.origin,
}, },
], ],

View File

@ -32,6 +32,7 @@ import {CDPElementHandle} from './ElementHandle.js';
import type {CommonEventEmitter} from './EventEmitter.js'; import type {CommonEventEmitter} from './EventEmitter.js';
import type {ExecutionContext} from './ExecutionContext.js'; import type {ExecutionContext} from './ExecutionContext.js';
import {CDPJSHandle} from './JSHandle.js'; import {CDPJSHandle} from './JSHandle.js';
import {Awaitable} from './types.js';
/** /**
* @internal * @internal
@ -381,7 +382,7 @@ export const isDate = (obj: unknown): obj is Date => {
export async function waitForEvent<T>( export async function waitForEvent<T>(
emitter: CommonEventEmitter, emitter: CommonEventEmitter,
eventName: string | symbol, eventName: string | symbol,
predicate: (event: T) => Promise<boolean> | boolean, predicate: (event: T) => Awaitable<boolean>,
timeout: number, timeout: number,
abortPromise: Promise<Error> | Deferred<Error> abortPromise: Promise<Error> | Deferred<Error>
): Promise<T> { ): Promise<T> {

View File

@ -39,3 +39,20 @@ export {
pipe, pipe,
Observable, Observable,
} from 'rxjs'; } from 'rxjs';
import {mergeMap, from, filter, map, type Observable} from 'rxjs';
export function filterAsync<T>(
predicate: (value: T) => boolean | PromiseLike<boolean>
) {
return mergeMap<T, Observable<T>>(value => {
return from(Promise.resolve(predicate(value))).pipe(
filter(isMatch => {
return isMatch;
}),
map(() => {
return value;
})
);
});
}

View File

@ -653,12 +653,30 @@
"parameters": ["webDriverBiDi"], "parameters": ["webDriverBiDi"],
"expectations": ["PASS"] "expectations": ["PASS"]
}, },
{
"testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.click should throw for <br> elements",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["PASS"]
},
{ {
"testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.click should throw for detached nodes", "testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.click should throw for detached nodes",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"], "parameters": ["webDriverBiDi"],
"expectations": ["PASS"] "expectations": ["PASS"]
}, },
{
"testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.click should throw for hidden nodes",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["PASS"]
},
{
"testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.click should throw for recursively hidden nodes",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["PASS"]
},
{ {
"testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.click should work", "testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.click should work",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
@ -671,6 +689,12 @@
"parameters": ["webDriverBiDi"], "parameters": ["webDriverBiDi"],
"expectations": ["PASS"] "expectations": ["PASS"]
}, },
{
"testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.clickablePoint should not work if the click box is not visible",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["PASS"]
},
{ {
"testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.clickablePoint should work", "testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.clickablePoint should work",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
@ -1049,12 +1073,6 @@
"parameters": ["firefox"], "parameters": ["firefox"],
"expectations": ["SKIP"] "expectations": ["SKIP"]
}, },
{
"testIdPattern": "[locator.spec] Locator Locator.click should work with a OOPIF",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["FAIL"]
},
{ {
"testIdPattern": "[mouse.spec] Mouse should click the document", "testIdPattern": "[mouse.spec] Mouse should click the document",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
@ -1919,6 +1937,18 @@
"parameters": ["cdp", "firefox"], "parameters": ["cdp", "firefox"],
"expectations": ["FAIL"] "expectations": ["FAIL"]
}, },
{
"testIdPattern": "[click.spec] Page.click should click the button inside an iframe",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"],
"expectations": ["PASS"]
},
{
"testIdPattern": "[click.spec] Page.click should click the button with deviceScaleFactor set",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"],
"expectations": ["PASS"]
},
{ {
"testIdPattern": "[click.spec] Page.click should click the button with fixed position inside an iframe", "testIdPattern": "[click.spec] Page.click should click the button with fixed position inside an iframe",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
@ -2099,18 +2129,6 @@
"parameters": ["cdp", "firefox"], "parameters": ["cdp", "firefox"],
"expectations": ["FAIL"] "expectations": ["FAIL"]
}, },
{
"testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.click should throw for hidden nodes",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"],
"expectations": ["PASS"]
},
{
"testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.click should throw for recursively hidden nodes",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"],
"expectations": ["PASS"]
},
{ {
"testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.clickablePoint should work for iframes", "testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.clickablePoint should work for iframes",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
@ -2147,12 +2165,6 @@
"parameters": ["cdp", "firefox"], "parameters": ["cdp", "firefox"],
"expectations": ["FAIL"] "expectations": ["FAIL"]
}, },
{
"testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.clickablePoint should not work if the click box is not visible",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"],
"expectations": ["PASS"]
},
{ {
"testIdPattern": "[emulation.spec] Emulation Page.emulateMediaFeatures should throw in case of bad argument", "testIdPattern": "[emulation.spec] Emulation Page.emulateMediaFeatures should throw in case of bad argument",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
@ -2747,6 +2759,18 @@
"parameters": ["firefox", "webDriverBiDi"], "parameters": ["firefox", "webDriverBiDi"],
"expectations": ["SKIP"] "expectations": ["SKIP"]
}, },
{
"testIdPattern": "[locator.spec] Locator Locator.click should work with a OOPIF",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"],
"expectations": ["PASS"]
},
{
"testIdPattern": "[locator.spec] Locator Locator.click should work with a OOPIF",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["firefox", "webDriverBiDi"],
"expectations": ["FAIL", "TIMEOUT"]
},
{ {
"testIdPattern": "[locator.spec] Locator Locator.race races multiple locators", "testIdPattern": "[locator.spec] Locator Locator.race races multiple locators",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
@ -3377,6 +3401,48 @@
"parameters": ["cdp", "firefox"], "parameters": ["cdp", "firefox"],
"expectations": ["FAIL"] "expectations": ["FAIL"]
}, },
{
"testIdPattern": "[oopif.spec] OOPIF clickablePoint, boundingBox, boxModel should work for elements inside OOPIFs",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"],
"expectations": ["PASS"]
},
{
"testIdPattern": "[oopif.spec] OOPIF should provide access to elements",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"],
"expectations": ["PASS"]
},
{
"testIdPattern": "[oopif.spec] OOPIF should support evaluating in oop iframes",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"],
"expectations": ["PASS"]
},
{
"testIdPattern": "[oopif.spec] OOPIF should support frames within OOP frames",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"],
"expectations": ["PASS"]
},
{
"testIdPattern": "[oopif.spec] OOPIF should support frames within OOP iframes",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"],
"expectations": ["PASS"]
},
{
"testIdPattern": "[oopif.spec] OOPIF should track navigations within OOP iframes",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"],
"expectations": ["PASS"]
},
{
"testIdPattern": "[oopif.spec] OOPIF should treat OOP iframes and normal iframes the same",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"],
"expectations": ["PASS"]
},
{ {
"testIdPattern": "[oopif.spec] OOPIF-debug OOPIF should support wait for navigation for transitions from local to OOPIF", "testIdPattern": "[oopif.spec] OOPIF-debug OOPIF should support wait for navigation for transitions from local to OOPIF",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],