From c4a44129201799f540ede8e12b1077b1d530c018 Mon Sep 17 00:00:00 2001 From: jrandolf <101637635+jrandolf@users.noreply.github.com> Date: Thu, 24 Aug 2023 20:32:29 +0200 Subject: [PATCH] chore: implement `waitForFrame` and use `clickablePoint` for ElementHandle operations (#10778) --- docs/api/puppeteer.elementhandle.click.md | 6 +- docs/api/puppeteer.elementhandle.hover.md | 2 +- docs/api/puppeteer.elementhandle.press.md | 5 +- docs/api/puppeteer.elementhandle.tap.md | 2 +- docs/api/puppeteer.elementhandle.touchend.md | 2 +- docs/api/puppeteer.elementhandle.touchmove.md | 2 +- .../api/puppeteer.elementhandle.touchstart.md | 2 +- docs/api/puppeteer.elementhandle.type.md | 5 +- docs/api/puppeteer.page.md | 2 +- docs/api/puppeteer.page.waitforframe.md | 24 ++-- .../puppeteer-core/src/api/ElementHandle.ts | 88 +++++++++----- packages/puppeteer-core/src/api/Page.ts | 64 +++++++--- .../src/common/ElementHandle.ts | 74 +----------- packages/puppeteer-core/src/common/Page.ts | 47 -------- .../src/common/bidi/ElementHandle.ts | 96 --------------- .../puppeteer-core/src/common/bidi/Input.ts | 20 +-- packages/puppeteer-core/src/common/util.ts | 3 +- .../puppeteer-core/third_party/rxjs/rxjs.ts | 17 +++ test/TestExpectations.json | 114 ++++++++++++++---- 19 files changed, 245 insertions(+), 330 deletions(-) diff --git a/docs/api/puppeteer.elementhandle.click.md b/docs/api/puppeteer.elementhandle.click.md index 568e9f40da9..bb9fcdea0d4 100644 --- a/docs/api/puppeteer.elementhandle.click.md +++ b/docs/api/puppeteer.elementhandle.click.md @@ -10,9 +10,9 @@ This method scrolls element into view if needed, and then uses [Page.mouse](./pu ```typescript class ElementHandle { - abstract click( + click( this: ElementHandle, - options?: ClickOptions + options?: Readonly ): Promise; } ``` @@ -22,7 +22,7 @@ class ElementHandle { | Parameter | Type | Description | | --------- | ------------------------------------------------------------ | ------------ | | this | [ElementHandle](./puppeteer.elementhandle.md)<Element> | | -| options | [ClickOptions](./puppeteer.clickoptions.md) | _(Optional)_ | +| options | Readonly<[ClickOptions](./puppeteer.clickoptions.md)> | _(Optional)_ | **Returns:** diff --git a/docs/api/puppeteer.elementhandle.hover.md b/docs/api/puppeteer.elementhandle.hover.md index 356742d3edf..af325f51d91 100644 --- a/docs/api/puppeteer.elementhandle.hover.md +++ b/docs/api/puppeteer.elementhandle.hover.md @@ -10,7 +10,7 @@ This method scrolls element into view if needed, and then uses [Page](./puppetee ```typescript class ElementHandle { - abstract hover(this: ElementHandle): Promise; + hover(this: ElementHandle): Promise; } ``` diff --git a/docs/api/puppeteer.elementhandle.press.md b/docs/api/puppeteer.elementhandle.press.md index c3267ef50af..acfb8cc55d3 100644 --- a/docs/api/puppeteer.elementhandle.press.md +++ b/docs/api/puppeteer.elementhandle.press.md @@ -10,10 +10,7 @@ Focuses the element, and then uses [Keyboard.down()](./puppeteer.keyboard.down.m ```typescript class ElementHandle { - abstract press( - key: KeyInput, - options?: Readonly - ): Promise; + press(key: KeyInput, options?: Readonly): Promise; } ``` diff --git a/docs/api/puppeteer.elementhandle.tap.md b/docs/api/puppeteer.elementhandle.tap.md index dcfdbe28f66..18eb8d08bbd 100644 --- a/docs/api/puppeteer.elementhandle.tap.md +++ b/docs/api/puppeteer.elementhandle.tap.md @@ -10,7 +10,7 @@ This method scrolls element into view if needed, and then uses [Touchscreen.tap( ```typescript class ElementHandle { - abstract tap(this: ElementHandle): Promise; + tap(this: ElementHandle): Promise; } ``` diff --git a/docs/api/puppeteer.elementhandle.touchend.md b/docs/api/puppeteer.elementhandle.touchend.md index 02e6bba96dc..037605aa2b4 100644 --- a/docs/api/puppeteer.elementhandle.touchend.md +++ b/docs/api/puppeteer.elementhandle.touchend.md @@ -8,7 +8,7 @@ sidebar_label: ElementHandle.touchEnd ```typescript class ElementHandle { - abstract touchEnd(this: ElementHandle): Promise; + touchEnd(this: ElementHandle): Promise; } ``` diff --git a/docs/api/puppeteer.elementhandle.touchmove.md b/docs/api/puppeteer.elementhandle.touchmove.md index e55a741ddce..f2221a7fc80 100644 --- a/docs/api/puppeteer.elementhandle.touchmove.md +++ b/docs/api/puppeteer.elementhandle.touchmove.md @@ -8,7 +8,7 @@ sidebar_label: ElementHandle.touchMove ```typescript class ElementHandle { - abstract touchMove(this: ElementHandle): Promise; + touchMove(this: ElementHandle): Promise; } ``` diff --git a/docs/api/puppeteer.elementhandle.touchstart.md b/docs/api/puppeteer.elementhandle.touchstart.md index 0dbfe450007..2fda4fe719e 100644 --- a/docs/api/puppeteer.elementhandle.touchstart.md +++ b/docs/api/puppeteer.elementhandle.touchstart.md @@ -8,7 +8,7 @@ sidebar_label: ElementHandle.touchStart ```typescript class ElementHandle { - abstract touchStart(this: ElementHandle): Promise; + touchStart(this: ElementHandle): Promise; } ``` diff --git a/docs/api/puppeteer.elementhandle.type.md b/docs/api/puppeteer.elementhandle.type.md index 48bb67fa22e..5682e3ee081 100644 --- a/docs/api/puppeteer.elementhandle.type.md +++ b/docs/api/puppeteer.elementhandle.type.md @@ -12,10 +12,7 @@ To press a special key, like `Control` or `ArrowDown`, use [ElementHandle.press( ```typescript class ElementHandle { - abstract type( - text: string, - options?: Readonly - ): Promise; + type(text: string, options?: Readonly): Promise; } ``` diff --git a/docs/api/puppeteer.page.md b/docs/api/puppeteer.page.md index b1351eb0b27..9974ae7147a 100644 --- a/docs/api/puppeteer.page.md +++ b/docs/api/puppeteer.page.md @@ -157,7 +157,7 @@ page.off('request', logRequest); | [viewport()](./puppeteer.page.viewport.md) | | Current page viewport settings. | | [waitForDevicePrompt(options)](./puppeteer.page.waitfordeviceprompt.md) | |

This method is typically coupled with an action that triggers a device request from an api such as WebBluetooth.

:::caution

This must be called before the device request is made. It will not return a currently active device prompt.

:::

| | [waitForFileChooser(options)](./puppeteer.page.waitforfilechooser.md) | |

This method is typically coupled with an action that triggers file choosing.

:::caution

This must be called before the file chooser is launched. It will not return a currently active file chooser.

:::

| -| [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. | | [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) | | | diff --git a/docs/api/puppeteer.page.waitforframe.md b/docs/api/puppeteer.page.waitforframe.md index 0950e0c28d1..67f98da454d 100644 --- a/docs/api/puppeteer.page.waitforframe.md +++ b/docs/api/puppeteer.page.waitforframe.md @@ -4,38 +4,30 @@ sidebar_label: Page.waitForFrame # Page.waitForFrame() method +Waits for a frame matching the given conditions to appear. + #### Signature: ```typescript class Page { waitForFrame( - urlOrPredicate: string | ((frame: Frame) => boolean | Promise), - options?: { - timeout?: number; - } + urlOrPredicate: string | ((frame: Frame) => Awaitable), + options?: WaitTimeoutOptions ): Promise; } ``` ## Parameters -| Parameter | Type | Description | -| -------------- | ------------------------------------------------------------------------------------------ | ---------------------------------------- | -| urlOrPredicate | string \| ((frame: [Frame](./puppeteer.frame.md)) => boolean \| Promise<boolean>) | A URL or predicate to wait for. | -| options | { timeout?: number; } | _(Optional)_ Optional waiting parameters | +| Parameter | Type | Description | +| -------------- | ------------------------------------------------------------------------------------------------------------- | ------------ | +| urlOrPredicate | string \| ((frame: [Frame](./puppeteer.frame.md)) => [Awaitable](./puppeteer.awaitable.md)<boolean>) | | +| options | [WaitTimeoutOptions](./puppeteer.waittimeoutoptions.md) | _(Optional)_ | **Returns:** Promise<[Frame](./puppeteer.frame.md)> -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 ```ts diff --git a/packages/puppeteer-core/src/api/ElementHandle.ts b/packages/puppeteer-core/src/api/ElementHandle.ts index 0e37cd61b85..65ba1e6a546 100644 --- a/packages/puppeteer-core/src/api/ElementHandle.ts +++ b/packages/puppeteer-core/src/api/ElementHandle.ts @@ -652,17 +652,25 @@ export abstract class ElementHandle< * uses {@link Page} to hover over the center of the element. * If the element is detached from DOM, the method throws an error. */ - abstract hover(this: ElementHandle): Promise; + async hover(this: ElementHandle): Promise { + 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 * 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. */ - abstract click( + async click( this: ElementHandle, - options?: ClickOptions - ): Promise; + options: Readonly = {} + ): Promise { + 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. @@ -804,13 +812,29 @@ export abstract class ElementHandle< * {@link Touchscreen.tap} to tap in the center of the element. * If the element is detached from DOM, the method throws an error. */ - abstract tap(this: ElementHandle): Promise; + async tap(this: ElementHandle): Promise { + 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): Promise; + async touchStart(this: ElementHandle): Promise { + await this.scrollIntoViewIfNeeded(); + const {x, y} = await this.clickablePoint(); + await this.frame.page().touchscreen.touchStart(x, y); + } - abstract touchMove(this: ElementHandle): Promise; + async touchMove(this: ElementHandle): Promise { + await this.scrollIntoViewIfNeeded(); + const {x, y} = await this.clickablePoint(); + await this.frame.page().touchscreen.touchMove(x, y); + } - abstract touchEnd(this: ElementHandle): Promise; + async touchEnd(this: ElementHandle): Promise { + 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. @@ -849,10 +873,13 @@ export abstract class ElementHandle< * * @param options - Delay in milliseconds. Defaults to 0. */ - abstract type( + async type( text: string, options?: Readonly - ): Promise; + ): Promise { + await this.focus(); + await this.frame.page().keyboard.type(text, options); + } /** * 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`. * See {@link KeyInput} for a list of all key names. */ - abstract press( + async press( key: KeyInput, options?: Readonly - ): Promise; + ): Promise { + await this.focus(); + await this.frame.page().keyboard.press(key, options); + } async #clickableBox(): Promise { const adoptedThis = await this.frame.isolatedRealm().adoptHandle(this); - const rects = await adoptedThis.evaluate(element => { + const boxes = await adoptedThis.evaluate(element => { if (!(element instanceof Element)) { return null; } return [...element.getClientRects()].map(rect => { - return rect.toJSON(); - }) as DOMRect[]; + return {x: rect.x, y: rect.y, width: rect.width, height: rect.height}; + }); }); void adoptedThis.dispose().catch(debugError); - if (!rects?.length) { + if (!boxes?.length) { return null; } - await this.#intersectBoundingBoxesWithFrame(rects); + await this.#intersectBoundingBoxesWithFrame(boxes); let frame: Frame | null | undefined = this.frame; let element: HandleFor | null | undefined; while ((element = await frame?.frameElement())) { @@ -914,27 +944,27 @@ export abstract class ElementHandle< if (!parentBox) { return null; } - for (const box of rects) { + for (const box of boxes) { box.x += parentBox.left; box.y += parentBox.top; } - await element.#intersectBoundingBoxesWithFrame(rects); + await element.#intersectBoundingBoxesWithFrame(boxes); frame = frame?.parentFrame(); } finally { void element.dispose().catch(debugError); } } - const rect = rects.find(box => { + const box = boxes.find(box => { return box.width >= 1 && box.height >= 1; }); - if (!rect) { + if (!box) { return null; } return { - x: rect.x, - y: rect.y, - height: rect.height, - width: rect.width, + x: box.x, + y: box.y, + height: box.height, + width: box.width, }; } @@ -967,7 +997,7 @@ export abstract class ElementHandle< return null; } 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); if (!box) { @@ -977,11 +1007,9 @@ export abstract class ElementHandle< if (!offset) { return null; } - box.x += offset.x; - box.y += offset.y; return { - x: box.x, - y: box.y, + x: box.x + offset.x, + y: box.y + offset.y, height: box.height, width: box.width, }; diff --git a/packages/puppeteer-core/src/api/Page.ts b/packages/puppeteer-core/src/api/Page.ts index 9d366f6b260..3a5f346e27c 100644 --- a/packages/puppeteer-core/src/api/Page.ts +++ b/packages/puppeteer-core/src/api/Page.ts @@ -18,6 +18,18 @@ import type {Readable} from 'stream'; 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 {HTTPResponse} from '../api/HTTPResponse.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 {Device} from '../common/Device.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 type {FileChooser} from '../common/FileChooser.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. - * @param options - Optional waiting parameters - * @returns Promise which resolves to the matched frame. + * Waits for a frame matching the given conditions to appear. + * * @example * * ```ts @@ -1755,20 +1766,41 @@ export class Page extends EventEmitter { * 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( - urlOrPredicate: string | ((frame: Frame) => boolean | Promise), - options?: {timeout?: number} - ): Promise; - async waitForFrame(): Promise { - throw new Error('Not implemented'); + urlOrPredicate: string | ((frame: Frame) => Awaitable), + options: WaitTimeoutOptions = {} + ): Promise { + const {timeout: ms = this.getDefaultTimeout()} = options; + + if (isString(urlOrPredicate)) { + urlOrPredicate = (frame: Frame) => { + return urlOrPredicate === frame.url(); + }; + } + + return firstValueFrom( + merge( + fromEvent(this, PageEmittedEvents.FrameAttached) as Observable, + fromEvent(this, PageEmittedEvents.FrameNavigated) as Observable, + 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.'); + }) + ) + ) + ) + ); } /** diff --git a/packages/puppeteer-core/src/common/ElementHandle.ts b/packages/puppeteer-core/src/common/ElementHandle.ts index 5b8f2c945a2..3c9797a6654 100644 --- a/packages/puppeteer-core/src/common/ElementHandle.ts +++ b/packages/puppeteer-core/src/common/ElementHandle.ts @@ -16,13 +16,7 @@ import {Protocol} from 'devtools-protocol'; -import { - AutofillData, - ClickOptions, - ElementHandle, - Point, -} from '../api/ElementHandle.js'; -import {KeyboardTypeOptions, KeyPressOptions} from '../api/Input.js'; +import {AutofillData, ElementHandle, Point} from '../api/ElementHandle.js'; import {Page, ScreenshotOptions} from '../api/Page.js'; import {assert} from '../util/assert.js'; @@ -33,7 +27,6 @@ import {FrameManager} from './FrameManager.js'; import {WaitForSelectorOptions} from './IsolatedWorld.js'; import {CDPJSHandle} from './JSHandle.js'; import {NodeFor} from './types.js'; -import {KeyInput} from './USKeyboardLayout.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): Promise { - 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, - options: Readonly = {} - ): Promise { - 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. */ @@ -281,46 +249,6 @@ export class CDPElementHandle< } } - override async tap(this: CDPElementHandle): Promise { - 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): Promise { - await this.scrollIntoViewIfNeeded(); - const {x, y} = await this.clickablePoint(); - await this.#page.touchscreen.touchStart(x, y); - } - - override async touchMove(this: CDPElementHandle): Promise { - await this.scrollIntoViewIfNeeded(); - const {x, y} = await this.clickablePoint(); - await this.#page.touchscreen.touchMove(x, y); - } - - override async touchEnd(this: CDPElementHandle): Promise { - await this.scrollIntoViewIfNeeded(); - await this.#page.touchscreen.touchEnd(); - } - - override async type( - text: string, - options?: Readonly - ): Promise { - await this.focus(); - await this.#page.keyboard.type(text, options); - } - - override async press( - key: KeyInput, - options?: Readonly - ): Promise { - await this.focus(); - await this.#page.keyboard.press(key, options); - } - override async screenshot( this: CDPElementHandle, options: ScreenshotOptions = {} diff --git a/packages/puppeteer-core/src/common/Page.ts b/packages/puppeteer-core/src/common/Page.ts index 86319543d6d..9b6e211de57 100644 --- a/packages/puppeteer-core/src/common/Page.ts +++ b/packages/puppeteer-core/src/common/Page.ts @@ -997,53 +997,6 @@ export class CDPPage extends Page { ); } - override async waitForFrame( - urlOrPredicate: string | ((frame: Frame) => boolean | Promise), - options: {timeout?: number} = {} - ): Promise { - const {timeout = this.#timeoutSettings.timeout()} = options; - - let predicate: (frame: Frame) => Promise; - 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 = 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( options: WaitForOptions = {} ): Promise { diff --git a/packages/puppeteer-core/src/common/bidi/ElementHandle.ts b/packages/puppeteer-core/src/common/bidi/ElementHandle.ts index f74283d66fe..a8ccd3ae72f 100644 --- a/packages/puppeteer-core/src/common/bidi/ElementHandle.ts +++ b/packages/puppeteer-core/src/common/bidi/ElementHandle.ts @@ -19,11 +19,7 @@ import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; import { AutofillData, ElementHandle as BaseElementHandle, - ClickOptions, } 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 {Frame} from './Frame.js'; @@ -105,96 +101,4 @@ export class ElementHandle< } return null; } - - // /////////////////// - // // Input methods // - // /////////////////// - override async click( - this: ElementHandle, - options?: Readonly - ): Promise { - 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): Promise { - 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): Promise { - 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): Promise { - 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): Promise { - 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): Promise { - await this.scrollIntoViewIfNeeded(); - await this.#frame.page().touchscreen.touchEnd(); - } - - override async type( - text: string, - options?: Readonly - ): Promise { - await this.focus(); - await this.#frame.page().keyboard.type(text, options); - } - - override async press( - key: KeyInput, - options?: Readonly - ): Promise { - await this.focus(); - await this.#frame.page().keyboard.press(key, options); - } } diff --git a/packages/puppeteer-core/src/common/bidi/Input.ts b/packages/puppeteer-core/src/common/bidi/Input.ts index 642cfdb0db2..852c357c457 100644 --- a/packages/puppeteer-core/src/common/bidi/Input.ts +++ b/packages/puppeteer-core/src/common/bidi/Input.ts @@ -483,9 +483,10 @@ export class Mouse extends BaseMouse { y: number, options: Readonly = {} ): Promise { + // 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 = { - x, - y, + x: Math.round(x), + y: Math.round(y), }; await this.#context.connection.send('input.performActions', { context: this.#context.id, @@ -496,8 +497,7 @@ export class Mouse extends BaseMouse { actions: [ { type: ActionType.PointerMove, - x, - y, + ...this.#lastMovePoint, duration: (options.steps ?? 0) * 50, origin: options.origin, }, @@ -551,8 +551,8 @@ export class Mouse extends BaseMouse { const actions: Bidi.Input.PointerSourceAction[] = [ { type: ActionType.PointerMove, - x, - y, + x: Math.round(x), + y: Math.round(y), origin: options.origin, }, ]; @@ -653,8 +653,8 @@ export class Touchscreen extends BaseTouchscreen { actions: [ { type: ActionType.PointerMove, - x, - y, + x: Math.round(x), + y: Math.round(y), origin: options.origin, }, { @@ -684,8 +684,8 @@ export class Touchscreen extends BaseTouchscreen { actions: [ { type: ActionType.PointerMove, - x, - y, + x: Math.round(x), + y: Math.round(y), origin: options.origin, }, ], diff --git a/packages/puppeteer-core/src/common/util.ts b/packages/puppeteer-core/src/common/util.ts index a2ec79f101c..4b00700f52b 100644 --- a/packages/puppeteer-core/src/common/util.ts +++ b/packages/puppeteer-core/src/common/util.ts @@ -32,6 +32,7 @@ import {CDPElementHandle} from './ElementHandle.js'; import type {CommonEventEmitter} from './EventEmitter.js'; import type {ExecutionContext} from './ExecutionContext.js'; import {CDPJSHandle} from './JSHandle.js'; +import {Awaitable} from './types.js'; /** * @internal @@ -381,7 +382,7 @@ export const isDate = (obj: unknown): obj is Date => { export async function waitForEvent( emitter: CommonEventEmitter, eventName: string | symbol, - predicate: (event: T) => Promise | boolean, + predicate: (event: T) => Awaitable, timeout: number, abortPromise: Promise | Deferred ): Promise { diff --git a/packages/puppeteer-core/third_party/rxjs/rxjs.ts b/packages/puppeteer-core/third_party/rxjs/rxjs.ts index 5a2f27bf488..f657023ecf8 100644 --- a/packages/puppeteer-core/third_party/rxjs/rxjs.ts +++ b/packages/puppeteer-core/third_party/rxjs/rxjs.ts @@ -39,3 +39,20 @@ export { pipe, Observable, } from 'rxjs'; + +import {mergeMap, from, filter, map, type Observable} from 'rxjs'; + +export function filterAsync( + predicate: (value: T) => boolean | PromiseLike +) { + return mergeMap>(value => { + return from(Promise.resolve(predicate(value))).pipe( + filter(isMatch => { + return isMatch; + }), + map(() => { + return value; + }) + ); + }); +} diff --git a/test/TestExpectations.json b/test/TestExpectations.json index 89baad5137d..b2337fdb69a 100644 --- a/test/TestExpectations.json +++ b/test/TestExpectations.json @@ -653,12 +653,30 @@ "parameters": ["webDriverBiDi"], "expectations": ["PASS"] }, + { + "testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.click should throw for
elements", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, { "testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.click should throw for detached nodes", "platforms": ["darwin", "linux", "win32"], "parameters": ["webDriverBiDi"], "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", "platforms": ["darwin", "linux", "win32"], @@ -671,6 +689,12 @@ "parameters": ["webDriverBiDi"], "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", "platforms": ["darwin", "linux", "win32"], @@ -1049,12 +1073,6 @@ "parameters": ["firefox"], "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", "platforms": ["darwin", "linux", "win32"], @@ -1919,6 +1937,18 @@ "parameters": ["cdp", "firefox"], "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", "platforms": ["darwin", "linux", "win32"], @@ -2099,18 +2129,6 @@ "parameters": ["cdp", "firefox"], "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", "platforms": ["darwin", "linux", "win32"], @@ -2147,12 +2165,6 @@ "parameters": ["cdp", "firefox"], "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", "platforms": ["darwin", "linux", "win32"], @@ -2747,6 +2759,18 @@ "parameters": ["firefox", "webDriverBiDi"], "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", "platforms": ["darwin", "linux", "win32"], @@ -3377,6 +3401,48 @@ "parameters": ["cdp", "firefox"], "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", "platforms": ["darwin", "linux", "win32"],