diff --git a/docs/api/puppeteer.jshandle.getproperty_2.md b/docs/api/puppeteer.jshandle.getproperty_2.md new file mode 100644 index 00000000..09923079 --- /dev/null +++ b/docs/api/puppeteer.jshandle.getproperty_2.md @@ -0,0 +1,25 @@ +--- +sidebar_label: JSHandle.getProperty_2 +--- + +# JSHandle.getProperty() method + +#### Signature: + +```typescript +class JSHandle { + getProperty( + propertyName: HandleOr + ): Promise>; +} +``` + +## Parameters + +| Parameter | Type | Description | +| ------------ | -------------------------------------------- | ----------- | +| propertyName | [HandleOr](./puppeteer.handleor.md)<K> | | + +**Returns:** + +Promise<[HandleFor](./puppeteer.handlefor.md)<T\[K\]>> diff --git a/docs/api/puppeteer.jshandle.md b/docs/api/puppeteer.jshandle.md index 4970d602..6bdc4364 100644 --- a/docs/api/puppeteer.jshandle.md +++ b/docs/api/puppeteer.jshandle.md @@ -34,15 +34,16 @@ const windowHandle = await page.evaluateHandle(() => window); ## Methods -| Method | Modifiers | Description | -| ---------------------------------------------------------------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [asElement()](./puppeteer.jshandle.aselement.md) | | | -| [dispose()](./puppeteer.jshandle.dispose.md) | | Releases the object referenced by the handle for garbage collection. | -| [evaluate(pageFunction, args)](./puppeteer.jshandle.evaluate.md) | | Evaluates the given function with the current handle as its first argument. | -| [evaluateHandle(pageFunction, args)](./puppeteer.jshandle.evaluatehandle.md) | | Evaluates the given function with the current handle as its first argument. | -| [getProperties()](./puppeteer.jshandle.getproperties.md) | | Gets a map of handles representing the properties of the current handle. | -| [getProperty(propertyName)](./puppeteer.jshandle.getproperty.md) | | Fetches a single property from the referenced object. | -| [getProperty(propertyName)](./puppeteer.jshandle.getproperty_1.md) | | | -| [jsonValue()](./puppeteer.jshandle.jsonvalue.md) | | | -| [remoteObject()](./puppeteer.jshandle.remoteobject.md) | | Provides access to the \[Protocol.Runtime.RemoteObject\](https://chromedevtools.github.io/devtools-protocol/tot/Runtime/\#type-RemoteObject) backing this handle. | -| [toString()](./puppeteer.jshandle.tostring.md) | | Returns a string representation of the JSHandle. | +| Method | Modifiers | Description | +| ---------------------------------------------------------------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------- | +| [asElement()](./puppeteer.jshandle.aselement.md) | | | +| [dispose()](./puppeteer.jshandle.dispose.md) | | Releases the object referenced by the handle for garbage collection. | +| [evaluate(pageFunction, args)](./puppeteer.jshandle.evaluate.md) | | Evaluates the given function with the current handle as its first argument. | +| [evaluateHandle(pageFunction, args)](./puppeteer.jshandle.evaluatehandle.md) | | Evaluates the given function with the current handle as its first argument. | +| [getProperties()](./puppeteer.jshandle.getproperties.md) | | Gets a map of handles representing the properties of the current handle. | +| [getProperty(propertyName)](./puppeteer.jshandle.getproperty.md) | | Fetches a single property from the referenced object. | +| [getProperty(propertyName)](./puppeteer.jshandle.getproperty_1.md) | | | +| [getProperty(propertyName)](./puppeteer.jshandle.getproperty_2.md) | | | +| [jsonValue()](./puppeteer.jshandle.jsonvalue.md) | | | +| [remoteObject()](./puppeteer.jshandle.remoteobject.md) | | Provides access to the \[Protocol.Runtime.RemoteObject\](https://chromedevtools.github.io/devtools-protocol/tot/Runtime/\#type-RemoteObject) | +| [toString()](./puppeteer.jshandle.tostring.md) | | Returns a string representation of the JSHandle. | diff --git a/docs/api/puppeteer.jshandle.remoteobject.md b/docs/api/puppeteer.jshandle.remoteobject.md index b0d73535..f8e12a98 100644 --- a/docs/api/puppeteer.jshandle.remoteobject.md +++ b/docs/api/puppeteer.jshandle.remoteobject.md @@ -4,7 +4,7 @@ sidebar_label: JSHandle.remoteObject # JSHandle.remoteObject() method -Provides access to the \[Protocol.Runtime.RemoteObject\](https://chromedevtools.github.io/devtools-protocol/tot/Runtime/\#type-RemoteObject) backing this handle. +Provides access to the \[Protocol.Runtime.RemoteObject\](https://chromedevtools.github.io/devtools-protocol/tot/Runtime/\#type-RemoteObject) #### Signature: diff --git a/packages/puppeteer-core/src/api/ElementHandle.ts b/packages/puppeteer-core/src/api/ElementHandle.ts new file mode 100644 index 00000000..6929d73d --- /dev/null +++ b/packages/puppeteer-core/src/api/ElementHandle.ts @@ -0,0 +1,731 @@ +/** + * 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 {Protocol} from 'devtools-protocol'; +import {Frame} from '../common/Frame.js'; +import {WaitForSelectorOptions} from '../common/IsolatedWorld.js'; +import {JSHandle} from './JSHandle.js'; +import {ScreenshotOptions} from './Page.js'; +import {ElementFor, EvaluateFunc, HandleFor, NodeFor} from '../common/types.js'; +import {KeyInput} from '../common/USKeyboardLayout.js'; +import {MouseButton} from '../common/Input.js'; +import {ExecutionContext} from '../common/ExecutionContext.js'; +import {CDPSession} from '../common/Connection.js'; + +/** + * @public + */ +export interface BoxModel { + content: Point[]; + padding: Point[]; + border: Point[]; + margin: Point[]; + width: number; + height: number; +} + +/** + * @public + */ +export interface BoundingBox extends Point { + /** + * the width of the element in pixels. + */ + width: number; + /** + * the height of the element in pixels. + */ + height: number; +} + +/** + * @public + */ +export interface Offset { + /** + * x-offset for the clickable point relative to the top-left corner of the border box. + */ + x: number; + /** + * y-offset for the clickable point relative to the top-left corner of the border box. + */ + y: number; +} + +/** + * @public + */ +export interface ClickOptions { + /** + * Time to wait between `mousedown` and `mouseup` in milliseconds. + * + * @defaultValue 0 + */ + delay?: number; + /** + * @defaultValue 'left' + */ + button?: MouseButton; + /** + * @defaultValue 1 + */ + clickCount?: number; + /** + * Offset for the clickable point relative to the top-left corner of the border box. + */ + offset?: Offset; +} + +/** + * @public + */ +export interface PressOptions { + /** + * Time to wait between `keydown` and `keyup` in milliseconds. Defaults to 0. + */ + delay?: number; + /** + * If specified, generates an input event with this text. + */ + text?: string; +} + +/** + * @public + */ +export interface Point { + x: number; + y: number; +} + +/** + * ElementHandle represents an in-page DOM element. + * + * @remarks + * ElementHandles can be created with the {@link Page.$} method. + * + * ```ts + * import puppeteer from 'puppeteer'; + * + * (async () => { + * const browser = await puppeteer.launch(); + * const page = await browser.newPage(); + * await page.goto('https://example.com'); + * const hrefElement = await page.$('a'); + * await hrefElement.click(); + * // ... + * })(); + * ``` + * + * ElementHandle prevents the DOM element from being garbage-collected unless the + * handle is {@link JSHandle.dispose | disposed}. ElementHandles are auto-disposed + * when their origin frame gets navigated. + * + * ElementHandle instances can be used as arguments in {@link Page.$eval} and + * {@link Page.evaluate} methods. + * + * If you're using TypeScript, ElementHandle takes a generic argument that + * denotes the type of element the handle is holding within. For example, if you + * have a handle to a `` element matching `selector`, the method + * throws an error. + * + * @example + * + * ```ts + * handle.select('blue'); // single selection + * handle.select('red', 'green', 'blue'); // multiple selections + * ``` + * + * @param values - Values of options to select. If the `` element, you can type it as - * `ElementHandle` and you get some nicer type checks. - * - * @public + * @internal */ - -export class ElementHandle< +export class CDPElementHandle< ElementType extends Node = Element -> extends JSHandle { +> extends ElementHandle { + #disposed = false; #frame: Frame; + #context: ExecutionContext; + #remoteObject: Protocol.Runtime.RemoteObject; - /** - * @internal - */ constructor( context: ExecutionContext, remoteObject: Protocol.Runtime.RemoteObject, frame: Frame ) { - super(context, remoteObject); + super(); + this.#context = context; + this.#remoteObject = remoteObject; this.#frame = frame; } + /** + * @internal + */ + override executionContext(): ExecutionContext { + return this.#context; + } + + /** + * @internal + */ + override get client(): CDPSession { + return this.#context._client; + } + + override remoteObject(): Protocol.Runtime.RemoteObject { + return this.#remoteObject; + } + + override async evaluate< + Params extends unknown[], + Func extends EvaluateFunc<[this, ...Params]> = EvaluateFunc< + [this, ...Params] + > + >( + pageFunction: string | Func, + ...args: Params + ): // @ts-expect-error Circularity here is okay because we only need the return + // type which doesn't use `this`. + Promise>> { + return this.executionContext().evaluate(pageFunction, this, ...args); + } + + override evaluateHandle< + Params extends unknown[], + Func extends EvaluateFunc<[this, ...Params]> = EvaluateFunc< + [this, ...Params] + > + >( + pageFunction: string | Func, + ...args: Params + ): // @ts-expect-error Circularity here is okay because we only need the return + // type which doesn't use `this`. + Promise>>> { + return this.executionContext().evaluateHandle(pageFunction, this, ...args); + } + get #frameManager(): FrameManager { return this.#frame._frameManager; } @@ -105,20 +139,50 @@ export class ElementHandle< return this.#frame.page(); } - get frame(): Frame { + override get frame(): Frame { return this.#frame; } - /** - * Queries the current element for an element matching the given selector. - * - * @param selector - The selector to query for. - * @returns A {@link ElementHandle | element handle} to the first element - * matching the given selector. Otherwise, `null`. - */ - async $( + override get disposed(): boolean { + return this.#disposed; + } + + override async getProperty( + propertyName: HandleOr + ): Promise>; + override async getProperty(propertyName: string): Promise>; + override async getProperty( + propertyName: HandleOr + ): Promise> { + return this.evaluateHandle((object, propertyName) => { + return object[propertyName as K]; + }, propertyName); + } + + override async jsonValue(): Promise { + if (!this.#remoteObject.objectId) { + return valueFromRemoteObject(this.#remoteObject); + } + const value = await this.evaluate(object => { + return object; + }); + if (value === undefined) { + throw new Error('Could not serialize referenced object'); + } + return value; + } + + override toString(): string { + if (!this.#remoteObject.objectId) { + return 'JSHandle:' + valueFromRemoteObject(this.#remoteObject); + } + const type = this.#remoteObject.subtype || this.#remoteObject.type; + return 'JSHandle@' + type; + } + + override async $( selector: Selector - ): Promise> | null> { + ): Promise> | null> { const {updatedSelector, queryHandler} = getQueryHandlerAndSelector(selector); assert( @@ -128,19 +192,12 @@ export class ElementHandle< return (await queryHandler.queryOne( this, updatedSelector - )) as ElementHandle> | null; + )) as CDPElementHandle> | null; } - /** - * Queries the current element for all elements matching the given selector. - * - * @param selector - The selector to query for. - * @returns An array of {@link ElementHandle | element handles} that point to - * elements matching the given selector. - */ - async $$( + override async $$( selector: Selector - ): Promise>>> { + ): Promise>>> { const {updatedSelector, queryHandler} = getQueryHandlerAndSelector(selector); assert( @@ -148,47 +205,23 @@ export class ElementHandle< 'Cannot handle queries for a multiple element with the given selector' ); return (await queryHandler.queryAll(this, updatedSelector)) as Array< - ElementHandle> + CDPElementHandle> >; } - /** - * Runs the given function on the first element matching the given selector in - * the current element. - * - * If the given function returns a promise, then this method will wait till - * the promise resolves. - * - * @example - * - * ```ts - * const tweetHandle = await page.$('.tweet'); - * expect(await tweetHandle.$eval('.like', node => node.innerText)).toBe( - * '100' - * ); - * expect(await tweetHandle.$eval('.retweets', node => node.innerText)).toBe( - * '10' - * ); - * ``` - * - * @param selector - The selector to query for. - * @param pageFunction - The function to be evaluated in this element's page's - * context. The first element matching the selector will be passed in as the - * first argument. - * @param args - Additional arguments to pass to `pageFunction`. - * @returns A promise to the result of the function. - */ - async $eval< + override async $eval< Selector extends string, Params extends unknown[], Func extends EvaluateFunc< - [ElementHandle>, ...Params] - > = EvaluateFunc<[ElementHandle>, ...Params]> + [CDPElementHandle>, ...Params] + > = EvaluateFunc<[CDPElementHandle>, ...Params]> >( selector: Selector, pageFunction: Func | string, ...args: Params - ): Promise>> { + ): // @ts-expect-error Circularity here is okay because we only need the return + // type which doesn't use `this`. + Promise>> { const elementHandle = await this.$(selector); if (!elementHandle) { throw new Error( @@ -200,40 +233,7 @@ export class ElementHandle< return result; } - /** - * Runs the given function on an array of elements matching the given selector - * in the current element. - * - * If the given function returns a promise, then this method will wait till - * the promise resolves. - * - * @example - * HTML: - * - * ```html - *
- *
Hello!
- *
Hi!
- *
- * ``` - * - * JavaScript: - * - * ```js - * const feedHandle = await page.$('.feed'); - * expect( - * await feedHandle.$$eval('.tweet', nodes => nodes.map(n => n.innerText)) - * ).toEqual(['Hello!', 'Hi!']); - * ``` - * - * @param selector - The selector to query for. - * @param pageFunction - The function to be evaluated in the element's page's - * context. An array of elements matching the given selector will be passed to - * the function as its first argument. - * @param args - Additional arguments to pass to `pageFunction`. - * @returns A promise to the result of the function. - */ - async $$eval< + override async $$eval< Selector extends string, Params extends unknown[], Func extends EvaluateFunc< @@ -267,66 +267,19 @@ export class ElementHandle< return result; } - /** - * @deprecated Use {@link ElementHandle.$$} with the `xpath` prefix. - * - * Example: `await elementHandle.$$('xpath/' + xpathExpression)` - * - * The method evaluates the XPath expression relative to the elementHandle. - * If `xpath` starts with `//` instead of `.//`, the dot will be appended - * automatically. - * - * If there are no such elements, the method will resolve to an empty array. - * @param expression - Expression to {@link https://developer.mozilla.org/en-US/docs/Web/API/Document/evaluate | evaluate} - */ - async $x(expression: string): Promise>> { + override async $x( + expression: string + ): Promise>> { if (expression.startsWith('//')) { expression = `.${expression}`; } return this.$$(`xpath/${expression}`); } - /** - * Wait for an element matching the given selector to appear in the current - * element. - * - * Unlike {@link Frame.waitForSelector}, this method does not work across - * navigations or if the element is detached from DOM. - * - * @example - * - * ```ts - * import puppeteer from 'puppeteer'; - * - * (async () => { - * const browser = await puppeteer.launch(); - * const page = await browser.newPage(); - * let currentURL; - * page - * .mainFrame() - * .waitForSelector('img') - * .then(() => console.log('First URL with image: ' + currentURL)); - * - * for (currentURL of [ - * 'https://example.com', - * 'https://google.com', - * 'https://bbc.com', - * ]) { - * await page.goto(currentURL); - * } - * await browser.close(); - * })(); - * ``` - * - * @param selector - The selector to query and wait for. - * @param options - Options for customizing waiting behavior. - * @returns An element matching the given selector. - * @throws Throws if an element matching the given selector doesn't appear. - */ - async waitForSelector( + override async waitForSelector( selector: Selector, options: WaitForSelectorOptions = {} - ): Promise> | null> { + ): Promise> | null> { const {updatedSelector, queryHandler} = getQueryHandlerAndSelector(selector); assert(queryHandler.waitFor, 'Query handler does not support waiting'); @@ -334,104 +287,24 @@ export class ElementHandle< this, updatedSelector, options - )) as ElementHandle> | null; + )) as CDPElementHandle> | null; } - /** - * @deprecated Use {@link ElementHandle.waitForSelector} with the `xpath` - * prefix. - * - * Example: `await elementHandle.waitForSelector('xpath/' + xpathExpression)` - * - * The method evaluates the XPath expression relative to the elementHandle. - * - * Wait for the `xpath` within the element. If at the moment of calling the - * method the `xpath` already exists, the method will return immediately. If - * the `xpath` doesn't appear after the `timeout` milliseconds of waiting, the - * function will throw. - * - * If `xpath` starts with `//` instead of `.//`, the dot will be appended - * automatically. - * - * This method works across navigation. - * - * ```ts - * import puppeteer from 'puppeteer'; - * (async () => { - * const browser = await puppeteer.launch(); - * const page = await browser.newPage(); - * let currentURL; - * page - * .waitForXPath('//img') - * .then(() => console.log('First URL with image: ' + currentURL)); - * for (currentURL of [ - * 'https://example.com', - * 'https://google.com', - * 'https://bbc.com', - * ]) { - * await page.goto(currentURL); - * } - * await browser.close(); - * })(); - * ``` - * - * @param xpath - A - * {@link https://developer.mozilla.org/en-US/docs/Web/XPath | xpath} of an - * element to wait for - * @param options - Optional waiting parameters - * @returns Promise which resolves when element specified by xpath string is - * added to DOM. Resolves to `null` if waiting for `hidden: true` and xpath is - * not found in DOM, otherwise resolves to `ElementHandle`. - * @remarks - * The optional Argument `options` have properties: - * - * - `visible`: A boolean to wait for element to be present in DOM and to be - * visible, i.e. to not have `display: none` or `visibility: hidden` CSS - * properties. Defaults to `false`. - * - * - `hidden`: A boolean wait for element to not be found in the DOM or to be - * hidden, i.e. have `display: none` or `visibility: hidden` CSS properties. - * Defaults to `false`. - * - * - `timeout`: A number which is maximum time to wait for in milliseconds. - * Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The - * default value can be changed by using the {@link Page.setDefaultTimeout} - * method. - */ - async waitForXPath( + override async waitForXPath( xpath: string, options: { visible?: boolean; hidden?: boolean; timeout?: number; } = {} - ): Promise | null> { + ): Promise | null> { if (xpath.startsWith('//')) { xpath = `.${xpath}`; } return this.waitForSelector(`xpath/${xpath}`, options); } - /** - * Converts the current handle to the given element type. - * - * @example - * - * ```ts - * const element: ElementHandle = await page.$( - * '.class-name-of-anchor' - * ); - * // DO NOT DISPOSE `element`, this will be always be the same handle. - * const anchor: ElementHandle = await element.toElement( - * 'a' - * ); - * ``` - * - * @param tagName - The tag name of the desired element type. - * @throws An error if the handle does not match. **The handle will not be - * automatically disposed.** - */ - async toElement< + override async toElement< K extends keyof HTMLElementTagNameMap | keyof SVGElementTagNameMap >(tagName: K): Promise>> { const isMatchingTagName = await this.evaluate((node, tagName) => { @@ -443,15 +316,11 @@ export class ElementHandle< return this as unknown as HandleFor>; } - override asElement(): ElementHandle | null { + override asElement(): CDPElementHandle | null { return this; } - /** - * Resolves to the content frame for element handles referencing - * iframe nodes, or null otherwise - */ - async contentFrame(): Promise { + override async contentFrame(): Promise { const nodeInfo = await this.client.send('DOM.describeNode', { objectId: this.remoteObject().objectId, }); @@ -461,7 +330,9 @@ export class ElementHandle< return this.#frameManager.frame(nodeInfo.node.frameId); } - async #scrollIntoViewIfNeeded(this: ElementHandle): Promise { + async #scrollIntoViewIfNeeded( + this: CDPElementHandle + ): Promise { const error = await this.evaluate( async (element): Promise => { if (!element.isConnected) { @@ -541,10 +412,7 @@ export class ElementHandle< return {offsetX, offsetY}; } - /** - * Returns the middle point within an element unless a specific offset is provided. - */ - async clickablePoint(offset?: Offset): Promise { + override async clickablePoint(offset?: Offset): Promise { const [result, layoutMetrics] = await Promise.all([ this.client .send('DOM.getContentQuads', { @@ -649,7 +517,7 @@ export class ElementHandle< * uses {@link Page.mouse} to hover over the center of the element. * If the element is detached from DOM, the method throws an error. */ - async hover(this: ElementHandle): Promise { + override async hover(this: CDPElementHandle): Promise { await this.#scrollIntoViewIfNeeded(); const {x, y} = await this.clickablePoint(); await this.#page.mouse.move(x, y); @@ -660,8 +528,8 @@ export class ElementHandle< * uses {@link Page.mouse} to click in the center of the element. * If the element is detached from DOM, the method throws an error. */ - async click( - this: ElementHandle, + override async click( + this: CDPElementHandle, options: ClickOptions = {} ): Promise { await this.#scrollIntoViewIfNeeded(); @@ -672,8 +540,8 @@ export class ElementHandle< /** * This method creates and captures a dragevent from the element. */ - async drag( - this: ElementHandle, + override async drag( + this: CDPElementHandle, target: Point ): Promise { assert( @@ -685,11 +553,8 @@ export class ElementHandle< return await this.#page.mouse.drag(start, target); } - /** - * This method creates a `dragenter` event on the element. - */ - async dragEnter( - this: ElementHandle, + override async dragEnter( + this: CDPElementHandle, data: Protocol.Input.DragData = {items: [], dragOperationsMask: 1} ): Promise { await this.#scrollIntoViewIfNeeded(); @@ -697,11 +562,8 @@ export class ElementHandle< await this.#page.mouse.dragEnter(target, data); } - /** - * This method creates a `dragover` event on the element. - */ - async dragOver( - this: ElementHandle, + override async dragOver( + this: CDPElementHandle, data: Protocol.Input.DragData = {items: [], dragOperationsMask: 1} ): Promise { await this.#scrollIntoViewIfNeeded(); @@ -709,11 +571,8 @@ export class ElementHandle< await this.#page.mouse.dragOver(target, data); } - /** - * This method triggers a drop on the element. - */ - async drop( - this: ElementHandle, + override async drop( + this: CDPElementHandle, data: Protocol.Input.DragData = {items: [], dragOperationsMask: 1} ): Promise { await this.#scrollIntoViewIfNeeded(); @@ -721,12 +580,9 @@ export class ElementHandle< await this.#page.mouse.drop(destination, data); } - /** - * This method triggers a dragenter, dragover, and drop on the element. - */ - async dragAndDrop( - this: ElementHandle, - target: ElementHandle, + override async dragAndDrop( + this: CDPElementHandle, + target: CDPElementHandle, options?: {delay: number} ): Promise { await this.#scrollIntoViewIfNeeded(); @@ -735,23 +591,7 @@ export class ElementHandle< await this.#page.mouse.dragAndDrop(startPoint, targetPoint, options); } - /** - * Triggers a `change` and `input` event once all the provided options have been - * selected. If there's no `` has the - * `multiple` attribute, all values are considered, otherwise only the first - * one is taken into account. - */ - async select(...values: string[]): Promise { + override async select(...values: string[]): Promise { for (const value of values) { assert( isString(value), @@ -795,18 +635,8 @@ export class ElementHandle< }, values); } - /** - * This method expects `elementHandle` to point to an - * {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input | input element}. - * - * @param filePaths - Sets the value of the file input to these paths. - * If a path is relative, then it is resolved against the - * {@link https://nodejs.org/api/process.html#process_process_cwd | current working directory}. - * Note for locals script connecting to remote chrome environments, - * paths must be absolute. - */ - async uploadFile( - this: ElementHandle, + override async uploadFile( + this: CDPElementHandle, ...filePaths: string[] ): Promise { const isMultiple = await this.evaluate(element => { @@ -837,7 +667,9 @@ export class ElementHandle< } }); const {objectId} = this.remoteObject(); - const {node} = await this.client.send('DOM.describeNode', {objectId}); + const {node} = await this.client.send('DOM.describeNode', { + objectId, + }); const {backendNodeId} = node; /* The zero-length array is a special case, it seems that @@ -861,39 +693,31 @@ export class ElementHandle< } } - /** - * This method scrolls element into view if needed, and then uses - * {@link Touchscreen.tap} to tap in the center of the element. - * If the element is detached from DOM, the method throws an error. - */ - async tap(this: ElementHandle): Promise { + 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(); } - async touchStart(this: ElementHandle): Promise { + override async touchStart(this: CDPElementHandle): Promise { await this.#scrollIntoViewIfNeeded(); const {x, y} = await this.clickablePoint(); await this.#page.touchscreen.touchStart(x, y); } - async touchMove(this: ElementHandle): Promise { + override async touchMove(this: CDPElementHandle): Promise { await this.#scrollIntoViewIfNeeded(); const {x, y} = await this.clickablePoint(); await this.#page.touchscreen.touchMove(x, y); } - async touchEnd(this: ElementHandle): Promise { + override async touchEnd(this: CDPElementHandle): Promise { await this.#scrollIntoViewIfNeeded(); await this.#page.touchscreen.touchEnd(); } - /** - * Calls {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus | focus} on the element. - */ - async focus(): Promise { + override async focus(): Promise { await this.evaluate(element => { if (!(element instanceof HTMLElement)) { throw new Error('Cannot focus non-HTMLElement'); @@ -902,58 +726,17 @@ export class ElementHandle< }); } - /** - * Focuses the element, and then sends a `keydown`, `keypress`/`input`, and - * `keyup` event for each character in the text. - * - * To press a special key, like `Control` or `ArrowDown`, - * use {@link ElementHandle.press}. - * - * @example - * - * ```ts - * await elementHandle.type('Hello'); // Types instantly - * await elementHandle.type('World', {delay: 100}); // Types slower, like a user - * ``` - * - * @example - * An example of typing into a text field and then submitting the form: - * - * ```ts - * const elementHandle = await page.$('input'); - * await elementHandle.type('some text'); - * await elementHandle.press('Enter'); - * ``` - */ - async type(text: string, options?: {delay: number}): Promise { + override async type(text: string, options?: {delay: number}): Promise { await this.focus(); await this.#page.keyboard.type(text, options); } - /** - * Focuses the element, and then uses {@link Keyboard.down} and {@link Keyboard.up}. - * - * @remarks - * If `key` is a single character and no modifier keys besides `Shift` - * are being held down, a `keypress`/`input` event will also be generated. - * The `text` option can be specified to force an input event to be generated. - * - * **NOTE** Modifier keys DO affect `elementHandle.press`. Holding down `Shift` - * will type the text in upper case. - * - * @param key - Name of key to press, such as `ArrowLeft`. - * See {@link KeyInput} for a list of all key names. - */ - async press(key: KeyInput, options?: PressOptions): Promise { + override async press(key: KeyInput, options?: PressOptions): Promise { await this.focus(); await this.#page.keyboard.press(key, options); } - /** - * This method returns the bounding box of the element (relative to the main frame), - * or `null` if the element is not visible. - */ - async boundingBox(): Promise { + override async boundingBox(): Promise { const result = await this.#getBoxModel(); if (!result) { @@ -970,15 +753,7 @@ export class ElementHandle< return {x: x + offsetX, y: y + offsetY, width, height}; } - /** - * This method returns boxes of the element, or `null` if the element is not visible. - * - * @remarks - * - * Boxes are represented as an array of points; - * Each Point is an object `{x, y}`. Box points are sorted clock-wise. - */ - async boxModel(): Promise { + override async boxModel(): Promise { const result = await this.#getBoxModel(); if (!result) { @@ -1014,13 +789,8 @@ export class ElementHandle< }; } - /** - * This method scrolls element into view if needed, and then uses - * {@link Page.screenshot} to take a screenshot of the element. - * If the element is detached from DOM, the method throws an error. - */ - async screenshot( - this: ElementHandle, + override async screenshot( + this: CDPElementHandle, options: ScreenshotOptions = {} ): Promise { let needsViewportReset = false; @@ -1077,11 +847,8 @@ export class ElementHandle< return imageData; } - /** - * Resolves to true if the element is visible in the current viewport. - */ - async isIntersectingViewport( - this: ElementHandle, + override async isIntersectingViewport( + this: CDPElementHandle, options?: { threshold?: number; } @@ -1098,6 +865,14 @@ export class ElementHandle< return threshold === 1 ? visibleRatio === 1 : visibleRatio > threshold; }, threshold); } + + override async dispose(): Promise { + if (this.#disposed) { + return; + } + this.#disposed = true; + await releaseObject(this.client, this.#remoteObject); + } } function computeQuadArea(quad: Point[]): number { diff --git a/packages/puppeteer-core/src/common/ExecutionContext.ts b/packages/puppeteer-core/src/common/ExecutionContext.ts index b8ae9cc7..f651656d 100644 --- a/packages/puppeteer-core/src/common/ExecutionContext.ts +++ b/packages/puppeteer-core/src/common/ExecutionContext.ts @@ -19,7 +19,7 @@ import {source as injectedSource} from '../generated/injected.js'; import type PuppeteerUtil from '../injected/injected.js'; import {CDPSession} from './Connection.js'; import {IsolatedWorld} from './IsolatedWorld.js'; -import {JSHandle} from './JSHandle.js'; +import {JSHandle} from '../api/JSHandle.js'; import {LazyArg} from './LazyArg.js'; import {EvaluateFunc, HandleFor} from './types.js'; import { diff --git a/packages/puppeteer-core/src/common/FileChooser.ts b/packages/puppeteer-core/src/common/FileChooser.ts index c0a68ff9..9b009d36 100644 --- a/packages/puppeteer-core/src/common/FileChooser.ts +++ b/packages/puppeteer-core/src/common/FileChooser.ts @@ -16,7 +16,7 @@ import {Protocol} from 'devtools-protocol'; import {assert} from '../util/assert.js'; -import {ElementHandle} from './ElementHandle.js'; +import {ElementHandle} from '../api/ElementHandle.js'; /** * File choosers let you react to the page requesting for a file. diff --git a/packages/puppeteer-core/src/common/Frame.ts b/packages/puppeteer-core/src/common/Frame.ts index 4c387d6c..e0afb5f8 100644 --- a/packages/puppeteer-core/src/common/Frame.ts +++ b/packages/puppeteer-core/src/common/Frame.ts @@ -18,7 +18,7 @@ import {Protocol} from 'devtools-protocol'; import {assert} from '../util/assert.js'; import {isErrorLike} from '../util/ErrorLike.js'; import {CDPSession} from './Connection.js'; -import {ElementHandle} from './ElementHandle.js'; +import {ElementHandle} from '../api/ElementHandle.js'; import {ExecutionContext} from './ExecutionContext.js'; import {FrameManager} from './FrameManager.js'; import {HTTPResponse} from './HTTPResponse.js'; diff --git a/packages/puppeteer-core/src/common/Input.ts b/packages/puppeteer-core/src/common/Input.ts index f181b6b6..95a07775 100644 --- a/packages/puppeteer-core/src/common/Input.ts +++ b/packages/puppeteer-core/src/common/Input.ts @@ -18,7 +18,7 @@ import {assert} from '../util/assert.js'; import {CDPSession} from './Connection.js'; import {_keyDefinitions, KeyDefinition, KeyInput} from './USKeyboardLayout.js'; import {Protocol} from 'devtools-protocol'; -import {Point} from './JSHandle.js'; +import {Point} from '../api/ElementHandle.js'; type KeyDescription = Required< Pick diff --git a/packages/puppeteer-core/src/common/IsolatedWorld.ts b/packages/puppeteer-core/src/common/IsolatedWorld.ts index 79480255..adedf4a1 100644 --- a/packages/puppeteer-core/src/common/IsolatedWorld.ts +++ b/packages/puppeteer-core/src/common/IsolatedWorld.ts @@ -24,14 +24,14 @@ import {Frame} from './Frame.js'; import {FrameManager} from './FrameManager.js'; import {MouseButton} from './Input.js'; import {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorlds.js'; -import {JSHandle} from './JSHandle.js'; +import {JSHandle} from '../api/JSHandle.js'; import {LifecycleWatcher, PuppeteerLifeCycleEvent} from './LifecycleWatcher.js'; import {TimeoutSettings} from './TimeoutSettings.js'; import {EvaluateFunc, HandleFor, InnerLazyParams, NodeFor} from './types.js'; import {createJSHandle, debugError, pageBindingInitString} from './util.js'; import {TaskManager, WaitTask} from './WaitTask.js'; -import type {ElementHandle} from './ElementHandle.js'; +import type {ElementHandle} from '../api/ElementHandle.js'; import {LazyArg} from './LazyArg.js'; /** diff --git a/packages/puppeteer-core/src/common/JSHandle.ts b/packages/puppeteer-core/src/common/JSHandle.ts index a9d6a83e..0ef70f22 100644 --- a/packages/puppeteer-core/src/common/JSHandle.ts +++ b/packages/puppeteer-core/src/common/JSHandle.ts @@ -15,64 +15,20 @@ */ import {Protocol} from 'devtools-protocol'; +import {JSHandle} from '../api/JSHandle.js'; import {assert} from '../util/assert.js'; import {CDPSession} from './Connection.js'; -import type {ElementHandle} from './ElementHandle.js'; +import type {CDPElementHandle} from './ElementHandle.js'; import {ExecutionContext} from './ExecutionContext.js'; -import {MouseButton} from './Input.js'; import {EvaluateFunc, HandleFor, HandleOr} from './types.js'; import {createJSHandle, releaseObject, valueFromRemoteObject} from './util.js'; declare const __JSHandleSymbol: unique symbol; /** - * @public + * @internal */ -export interface BoxModel { - content: Point[]; - padding: Point[]; - border: Point[]; - margin: Point[]; - width: number; - height: number; -} - -/** - * @public - */ -export interface BoundingBox extends Point { - /** - * the width of the element in pixels. - */ - width: number; - /** - * the height of the element in pixels. - */ - height: number; -} - -/** - * Represents a reference to a JavaScript object. Instances can be created using - * {@link Page.evaluateHandle}. - * - * Handles prevent the referenced JavaScript object from being garbage-collected - * unless the handle is purposely {@link JSHandle.dispose | disposed}. JSHandles - * are auto-disposed when their associated frame is navigated away or the parent - * context gets destroyed. - * - * Handles can be used as arguments for any evaluation function such as - * {@link Page.$eval}, {@link Page.evaluate}, and {@link Page.evaluateHandle}. - * They are resolved to their referenced object. - * - * @example - * - * ```ts - * const windowHandle = await page.evaluateHandle(() => window); - * ``` - * - * @public - */ -export class JSHandle { +export class CDPJSHandle extends JSHandle { /** * Used for nominally typing {@link JSHandle}. */ @@ -82,44 +38,31 @@ export class JSHandle { #context: ExecutionContext; #remoteObject: Protocol.Runtime.RemoteObject; - /** - * @internal - */ - get client(): CDPSession { - return this.#context._client; - } - - /** - * @internal - */ - get disposed(): boolean { + override get disposed(): boolean { return this.#disposed; } - /** - * @internal - */ constructor( context: ExecutionContext, remoteObject: Protocol.Runtime.RemoteObject ) { + super(); this.#context = context; this.#remoteObject = remoteObject; } - /** - * @internal - */ - executionContext(): ExecutionContext { + override executionContext(): ExecutionContext { return this.#context; } + override get client(): CDPSession { + return this.#context._client; + } + /** - * Evaluates the given function with the current handle as its first argument. - * * @see {@link ExecutionContext.evaluate} for more details. */ - async evaluate< + override async evaluate< Params extends unknown[], Func extends EvaluateFunc<[this, ...Params]> = EvaluateFunc< [this, ...Params] @@ -134,11 +77,9 @@ export class JSHandle { } /** - * Evaluates the given function with the current handle as its first argument. - * * @see {@link ExecutionContext.evaluateHandle} for more details. */ - async evaluateHandle< + override async evaluateHandle< Params extends unknown[], Func extends EvaluateFunc<[this, ...Params]> = EvaluateFunc< [this, ...Params] @@ -156,14 +97,11 @@ export class JSHandle { ); } - /** - * Fetches a single property from the referenced object. - */ - async getProperty( + override async getProperty( propertyName: HandleOr ): Promise>; - async getProperty(propertyName: string): Promise>; - async getProperty( + override async getProperty(propertyName: string): Promise>; + override async getProperty( propertyName: HandleOr ): Promise> { return this.evaluateHandle((object, propertyName) => { @@ -171,25 +109,7 @@ export class JSHandle { }, propertyName); } - /** - * Gets a map of handles representing the properties of the current handle. - * - * @example - * - * ```ts - * const listHandle = await page.evaluateHandle(() => document.body.children); - * const properties = await listHandle.getProperties(); - * const children = []; - * for (const property of properties.values()) { - * const element = property.asElement(); - * if (element) { - * children.push(element); - * } - * } - * children; // holds elementHandles to all children of document.body - * ``` - */ - async getProperties(): Promise> { + override async getProperties(): Promise> { assert(this.#remoteObject.objectId); // We use Runtime.getProperties rather than iterative building because the // iterative approach might create a distorted snapshot. @@ -207,15 +127,7 @@ export class JSHandle { return result; } - /** - * @returns A vanilla object representing the serializable portions of the - * referenced object. - * @throws Throws if the object cannot be serialized due to circularity. - * - * @remarks - * If the object has a `toJSON` function, it **will not** be called. - */ - async jsonValue(): Promise { + override async jsonValue(): Promise { if (!this.#remoteObject.objectId) { return valueFromRemoteObject(this.#remoteObject); } @@ -232,14 +144,11 @@ export class JSHandle { * @returns Either `null` or the handle itself if the handle is an * instance of {@link ElementHandle}. */ - asElement(): ElementHandle | null { + override asElement(): CDPElementHandle | null { return null; } - /** - * Releases the object referenced by the handle for garbage collection. - */ - async dispose(): Promise { + override async dispose(): Promise { if (this.#disposed) { return; } @@ -247,13 +156,7 @@ export class JSHandle { await releaseObject(this.client, this.#remoteObject); } - /** - * Returns a string representation of the JSHandle. - * - * @remarks - * Useful during debugging. - */ - toString(): string { + override toString(): string { if (!this.#remoteObject.objectId) { return 'JSHandle:' + valueFromRemoteObject(this.#remoteObject); } @@ -261,72 +164,7 @@ export class JSHandle { return 'JSHandle@' + type; } - /** - * Provides access to the - * [Protocol.Runtime.RemoteObject](https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#type-RemoteObject) - * backing this handle. - */ - remoteObject(): Protocol.Runtime.RemoteObject { + override remoteObject(): Protocol.Runtime.RemoteObject { return this.#remoteObject; } } - -/** - * @public - */ -export interface Offset { - /** - * x-offset for the clickable point relative to the top-left corner of the border box. - */ - x: number; - /** - * y-offset for the clickable point relative to the top-left corner of the border box. - */ - y: number; -} - -/** - * @public - */ -export interface ClickOptions { - /** - * Time to wait between `mousedown` and `mouseup` in milliseconds. - * - * @defaultValue 0 - */ - delay?: number; - /** - * @defaultValue 'left' - */ - button?: MouseButton; - /** - * @defaultValue 1 - */ - clickCount?: number; - /** - * Offset for the clickable point relative to the top-left corner of the border box. - */ - offset?: Offset; -} - -/** - * @public - */ -export interface PressOptions { - /** - * Time to wait between `keydown` and `keyup` in milliseconds. Defaults to 0. - */ - delay?: number; - /** - * If specified, generates an input event with this text. - */ - text?: string; -} - -/** - * @public - */ -export interface Point { - x: number; - y: number; -} diff --git a/packages/puppeteer-core/src/common/Page.ts b/packages/puppeteer-core/src/common/Page.ts index 480a84a4..e3cc6041 100644 --- a/packages/puppeteer-core/src/common/Page.ts +++ b/packages/puppeteer-core/src/common/Page.ts @@ -44,7 +44,7 @@ import { import {ConsoleMessage, ConsoleMessageType} from './ConsoleMessage.js'; import {Coverage} from './Coverage.js'; import {Dialog} from './Dialog.js'; -import {ElementHandle} from './ElementHandle.js'; +import {ElementHandle} from '../api/ElementHandle.js'; import {EmulationManager} from './EmulationManager.js'; import {FileChooser} from './FileChooser.js'; import { @@ -59,7 +59,7 @@ import {HTTPResponse} from './HTTPResponse.js'; import {Keyboard, Mouse, MouseButton, Touchscreen} from './Input.js'; import {WaitForSelectorOptions} from './IsolatedWorld.js'; import {MAIN_WORLD} from './IsolatedWorlds.js'; -import {JSHandle} from './JSHandle.js'; +import {JSHandle} from '../api/JSHandle.js'; import { Credentials, NetworkConditions, diff --git a/packages/puppeteer-core/src/common/QueryHandler.ts b/packages/puppeteer-core/src/common/QueryHandler.ts index 04c8f387..b9cc3d7e 100644 --- a/packages/puppeteer-core/src/common/QueryHandler.ts +++ b/packages/puppeteer-core/src/common/QueryHandler.ts @@ -17,7 +17,7 @@ import PuppeteerUtil from '../injected/injected.js'; import {assert} from '../util/assert.js'; import {ariaHandler} from './AriaQueryHandler.js'; -import {ElementHandle} from './ElementHandle.js'; +import {ElementHandle} from '../api/ElementHandle.js'; import {Frame} from './Frame.js'; import {WaitForSelectorOptions} from './IsolatedWorld.js'; import {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorlds.js'; diff --git a/packages/puppeteer-core/src/common/WaitTask.ts b/packages/puppeteer-core/src/common/WaitTask.ts index f08d6936..e5aac3f8 100644 --- a/packages/puppeteer-core/src/common/WaitTask.ts +++ b/packages/puppeteer-core/src/common/WaitTask.ts @@ -16,10 +16,10 @@ import type {Poller} from '../injected/Poller.js'; import {createDeferredPromise} from '../util/DeferredPromise.js'; -import {ElementHandle} from './ElementHandle.js'; +import {ElementHandle} from '../api/ElementHandle.js'; import {TimeoutError} from './Errors.js'; import {IsolatedWorld} from './IsolatedWorld.js'; -import {JSHandle} from './JSHandle.js'; +import {JSHandle} from '../api/JSHandle.js'; import {LazyArg} from './LazyArg.js'; import {HandleFor} from './types.js'; diff --git a/packages/puppeteer-core/src/common/WebWorker.ts b/packages/puppeteer-core/src/common/WebWorker.ts index cfc029c7..21fb8831 100644 --- a/packages/puppeteer-core/src/common/WebWorker.ts +++ b/packages/puppeteer-core/src/common/WebWorker.ts @@ -19,7 +19,8 @@ import {ConsoleMessageType} from './ConsoleMessage.js'; import {EvaluateFunc, HandleFor} from './types.js'; import {EventEmitter} from './EventEmitter.js'; import {ExecutionContext} from './ExecutionContext.js'; -import {JSHandle} from './JSHandle.js'; +import {JSHandle} from '../api/JSHandle.js'; +import {CDPJSHandle} from './JSHandle.js'; import {debugError} from './util.js'; import {createDeferredPromise} from '../util/DeferredPromise.js'; @@ -93,7 +94,7 @@ export class WebWorker extends EventEmitter { return consoleAPICalled( event.type, event.args.map((object: Protocol.Runtime.RemoteObject) => { - return new JSHandle(context, object); + return new CDPJSHandle(context, object); }), event.stackTrace ); diff --git a/packages/puppeteer-core/src/common/types.ts b/packages/puppeteer-core/src/common/types.ts index ed30e735..9f033058 100644 --- a/packages/puppeteer-core/src/common/types.ts +++ b/packages/puppeteer-core/src/common/types.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import {JSHandle} from './JSHandle.js'; -import {ElementHandle} from './ElementHandle.js'; +import {JSHandle} from '../api/JSHandle.js'; +import {ElementHandle} from '../api/ElementHandle.js'; import {LazyArg} from './LazyArg.js'; /** diff --git a/packages/puppeteer-core/src/common/util.ts b/packages/puppeteer-core/src/common/util.ts index 70bf91b7..b72361f7 100644 --- a/packages/puppeteer-core/src/common/util.ts +++ b/packages/puppeteer-core/src/common/util.ts @@ -21,12 +21,13 @@ import {assert} from '../util/assert.js'; import {isErrorLike} from '../util/ErrorLike.js'; import {CDPSession} from './Connection.js'; import {debug} from './Debug.js'; -import {ElementHandle} from './ElementHandle.js'; +import {ElementHandle} from '../api/ElementHandle.js'; +import {CDPElementHandle} from './ElementHandle.js'; import {TimeoutError} from './Errors.js'; import {CommonEventEmitter} from './EventEmitter.js'; import {ExecutionContext} from './ExecutionContext.js'; -import {JSHandle} from './JSHandle.js'; - +import {JSHandle} from '../api/JSHandle.js'; +import {CDPJSHandle} from './JSHandle.js'; /** * @internal */ @@ -239,9 +240,9 @@ export function createJSHandle( remoteObject: Protocol.Runtime.RemoteObject ): JSHandle | ElementHandle { if (remoteObject.subtype === 'node' && context._world) { - return new ElementHandle(context, remoteObject, context._world.frame()); + return new CDPElementHandle(context, remoteObject, context._world.frame()); } - return new JSHandle(context, remoteObject); + return new CDPJSHandle(context, remoteObject); } /** diff --git a/test/src/ariaqueryhandler.spec.ts b/test/src/ariaqueryhandler.spec.ts index 702c6058..a5032361 100644 --- a/test/src/ariaqueryhandler.spec.ts +++ b/test/src/ariaqueryhandler.spec.ts @@ -21,7 +21,7 @@ import { setupTestPageAndContextHooks, } from './mocha-utils.js'; -import {ElementHandle} from 'puppeteer-core/internal/common/ElementHandle.js'; +import type {ElementHandle} from 'puppeteer-core/internal/api/ElementHandle.js'; import utils from './utils.js'; import assert from 'assert'; import {TimeoutError} from 'puppeteer'; diff --git a/test/src/elementhandle.spec.ts b/test/src/elementhandle.spec.ts index 0d6a4fec..5c3df462 100644 --- a/test/src/elementhandle.spec.ts +++ b/test/src/elementhandle.spec.ts @@ -15,7 +15,7 @@ */ import expect from 'expect'; -import {ElementHandle} from 'puppeteer-core/internal/common/ElementHandle.js'; +import {ElementHandle} from 'puppeteer-core/internal/api/ElementHandle.js'; import sinon from 'sinon'; import { getTestState, diff --git a/test/src/idle_override.spec.ts b/test/src/idle_override.spec.ts index 83d7ca09..9b66e3d1 100644 --- a/test/src/idle_override.spec.ts +++ b/test/src/idle_override.spec.ts @@ -15,7 +15,7 @@ */ import expect from 'expect'; -import {ElementHandle} from 'puppeteer-core/internal/common/ElementHandle.js'; +import {ElementHandle} from 'puppeteer-core/internal/api/ElementHandle.js'; import { getTestState, setupTestBrowserHooks, diff --git a/test/src/queryhandler.spec.ts b/test/src/queryhandler.spec.ts index 462b9414..a8402937 100644 --- a/test/src/queryhandler.spec.ts +++ b/test/src/queryhandler.spec.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import expect from 'expect'; -import {ElementHandle} from 'puppeteer-core/internal/common/ElementHandle.js'; +import {ElementHandle} from 'puppeteer-core/internal/api/ElementHandle.js'; import { getTestState, setupTestBrowserHooks,