diff --git a/docs/api/puppeteer.jshandle.md b/docs/api/puppeteer.jshandle.md index 6bdc4364b99..4abf6f08343 100644 --- a/docs/api/puppeteer.jshandle.md +++ b/docs/api/puppeteer.jshandle.md @@ -34,16 +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) | | | -| [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. | +| 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) backing this handle. | +| [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 f8e12a9888e..b0d73535737 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) +Provides access to the \[Protocol.Runtime.RemoteObject\](https://chromedevtools.github.io/devtools-protocol/tot/Runtime/\#type-RemoteObject) backing this handle. #### Signature: diff --git a/packages/puppeteer-core/src/api/JSHandle.ts b/packages/puppeteer-core/src/api/JSHandle.ts index ec2d1f53fac..e11c52b0728 100644 --- a/packages/puppeteer-core/src/api/JSHandle.ts +++ b/packages/puppeteer-core/src/api/JSHandle.ts @@ -177,9 +177,17 @@ export class JSHandle { throw new Error('Not implemented'); } + /** + * @internal + */ + get id(): string | undefined { + throw new Error('Not implemented'); + } + /** * Provides access to the * [Protocol.Runtime.RemoteObject](https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#type-RemoteObject) + * backing this handle. */ remoteObject(): Protocol.Runtime.RemoteObject { throw new Error('Not implemented'); diff --git a/packages/puppeteer-core/src/common/Accessibility.ts b/packages/puppeteer-core/src/common/Accessibility.ts index daff5b96565..3960a8eb10d 100644 --- a/packages/puppeteer-core/src/common/Accessibility.ts +++ b/packages/puppeteer-core/src/common/Accessibility.ts @@ -186,7 +186,7 @@ export class Accessibility { let backendNodeId: number | undefined; if (root) { const {node} = await this.#client.send('DOM.describeNode', { - objectId: root.remoteObject().objectId, + objectId: root.id, }); backendNodeId = node.backendNodeId; } diff --git a/packages/puppeteer-core/src/common/AriaQueryHandler.ts b/packages/puppeteer-core/src/common/AriaQueryHandler.ts index bfcea4bd4ca..066c1a619c7 100644 --- a/packages/puppeteer-core/src/common/AriaQueryHandler.ts +++ b/packages/puppeteer-core/src/common/AriaQueryHandler.ts @@ -32,7 +32,7 @@ const queryAXTree = async ( role?: string ): Promise => { const {nodes} = await client.send('Accessibility.queryAXTree', { - objectId: element.remoteObject().objectId, + objectId: element.id, accessibleName, role, }); diff --git a/packages/puppeteer-core/src/common/ElementHandle.ts b/packages/puppeteer-core/src/common/ElementHandle.ts index 19b574ea873..fee5c3769fb 100644 --- a/packages/puppeteer-core/src/common/ElementHandle.ts +++ b/packages/puppeteer-core/src/common/ElementHandle.ts @@ -43,12 +43,8 @@ import { NodeFor, } from './types.js'; import {KeyInput} from './USKeyboardLayout.js'; -import { - debugError, - isString, - releaseObject, - valueFromRemoteObject, -} from './util.js'; +import {debugError, isString} from './util.js'; +import {CDPJSHandle} from './JSHandle.js'; const applyOffsetsToQuad = ( quad: Point[], @@ -70,10 +66,8 @@ const applyOffsetsToQuad = ( export class CDPElementHandle< ElementType extends Node = Element > extends ElementHandle { - #disposed = false; #frame: Frame; - #context: ExecutionContext; - #remoteObject: Protocol.Runtime.RemoteObject; + #jsHandle: CDPJSHandle; constructor( context: ExecutionContext, @@ -81,8 +75,7 @@ export class CDPElementHandle< frame: Frame ) { super(); - this.#context = context; - this.#remoteObject = remoteObject; + this.#jsHandle = new CDPJSHandle(context, remoteObject); this.#frame = frame; } @@ -90,18 +83,22 @@ export class CDPElementHandle< * @internal */ override executionContext(): ExecutionContext { - return this.#context; + return this.#jsHandle.executionContext(); } /** * @internal */ override get client(): CDPSession { - return this.#context._client; + return this.#jsHandle.client; + } + + override get id(): string | undefined { + return this.#jsHandle.id; } override remoteObject(): Protocol.Runtime.RemoteObject { - return this.#remoteObject; + return this.#jsHandle.remoteObject(); } override async evaluate< @@ -143,7 +140,7 @@ export class CDPElementHandle< } override get disposed(): boolean { - return this.#disposed; + return this.#jsHandle.disposed; } override async getProperty( @@ -153,30 +150,27 @@ export class CDPElementHandle< override async getProperty( propertyName: HandleOr ): Promise> { - return this.evaluateHandle((object, propertyName) => { - return object[propertyName as K]; - }, propertyName); + return this.#jsHandle.getProperty(propertyName); + } + + override async getProperties(): Promise> { + return this.#jsHandle.getProperties(); + } + + override asElement(): CDPElementHandle | null { + return this; } 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; + return this.#jsHandle.jsonValue(); } override toString(): string { - if (!this.#remoteObject.objectId) { - return 'JSHandle:' + valueFromRemoteObject(this.#remoteObject); - } - const type = this.#remoteObject.subtype || this.#remoteObject.type; - return 'JSHandle@' + type; + return this.#jsHandle.toString(); + } + + override async dispose(): Promise { + return await this.#jsHandle.dispose(); } override async $( @@ -297,10 +291,6 @@ export class CDPElementHandle< return this as unknown as HandleFor>; } - override asElement(): CDPElementHandle | null { - return this; - } - override async contentFrame(): Promise { const nodeInfo = await this.client.send('DOM.describeNode', { objectId: this.remoteObject().objectId, @@ -464,7 +454,7 @@ export class CDPElementHandle< #getBoxModel(): Promise { const params: Protocol.DOM.GetBoxModelRequest = { - objectId: this.remoteObject().objectId, + objectId: this.id, }; return this.client.send('DOM.getBoxModel', params).catch(error => { return debugError(error); @@ -846,14 +836,6 @@ export class CDPElementHandle< 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 f651656d1f2..0a41584b70c 100644 --- a/packages/puppeteer-core/src/common/ExecutionContext.ts +++ b/packages/puppeteer-core/src/common/ExecutionContext.ts @@ -29,6 +29,8 @@ import { stringifyFunction, valueFromRemoteObject, } from './util.js'; +import {CDPJSHandle} from './JSHandle.js'; +import {CDPElementHandle} from './ElementHandle.js'; /** * @public @@ -321,7 +323,10 @@ export class ExecutionContext { if (Object.is(arg, NaN)) { return {unserializableValue: 'NaN'}; } - const objectHandle = arg && arg instanceof JSHandle ? arg : null; + const objectHandle = + arg && (arg instanceof CDPJSHandle || arg instanceof CDPElementHandle) + ? arg + : null; if (objectHandle) { if (objectHandle.executionContext() !== this) { throw new Error( diff --git a/packages/puppeteer-core/src/common/IsolatedWorld.ts b/packages/puppeteer-core/src/common/IsolatedWorld.ts index 64425f72a32..ee5b56c7b9a 100644 --- a/packages/puppeteer-core/src/common/IsolatedWorld.ts +++ b/packages/puppeteer-core/src/common/IsolatedWorld.ts @@ -539,7 +539,7 @@ export class IsolatedWorld { 'Cannot adopt handle that already belongs to this execution context' ); const nodeInfo = await this.#client.send('DOM.describeNode', { - objectId: handle.remoteObject().objectId, + objectId: handle.id, }); return (await this.adoptBackendNode(nodeInfo.node.backendNodeId)) as T; } diff --git a/packages/puppeteer-core/src/common/JSHandle.ts b/packages/puppeteer-core/src/common/JSHandle.ts index a6f8256db62..2f89767829a 100644 --- a/packages/puppeteer-core/src/common/JSHandle.ts +++ b/packages/puppeteer-core/src/common/JSHandle.ts @@ -156,6 +156,10 @@ export class CDPJSHandle extends JSHandle { return 'JSHandle@' + type; } + override get id(): string | undefined { + return this.#remoteObject.objectId; + } + override remoteObject(): Protocol.Runtime.RemoteObject { return this.#remoteObject; } diff --git a/packages/puppeteer-core/src/common/Page.ts b/packages/puppeteer-core/src/common/Page.ts index 00c7272bb47..c9286a28ab5 100644 --- a/packages/puppeteer-core/src/common/Page.ts +++ b/packages/puppeteer-core/src/common/Page.ts @@ -525,13 +525,12 @@ export class CDPPage extends Page { ): Promise> { const context = await this.mainFrame().executionContext(); assert(!prototypeHandle.disposed, 'Prototype JSHandle is disposed!'); - const remoteObject = prototypeHandle.remoteObject(); assert( - remoteObject.objectId, + prototypeHandle.id, 'Prototype JSHandle must not be referencing primitive value' ); const response = await context._client.send('Runtime.queryObjects', { - prototypeObjectId: remoteObject.objectId, + prototypeObjectId: prototypeHandle.id, }); return createJSHandle(context, response.objects) as HandleFor; } @@ -820,7 +819,7 @@ export class CDPPage extends Page { } const textTokens = []; for (const arg of args) { - const remoteObject = arg.remoteObject(); + const remoteObject = arg.remoteObject() as Protocol.Runtime.RemoteObject; if (remoteObject.objectId) { textTokens.push(arg.toString()); } else { diff --git a/packages/puppeteer-core/src/common/bidi/Connection.ts b/packages/puppeteer-core/src/common/bidi/Connection.ts index c21e71dbd5e..ce0a0eb3e66 100644 --- a/packages/puppeteer-core/src/common/bidi/Connection.ts +++ b/packages/puppeteer-core/src/common/bidi/Connection.ts @@ -36,6 +36,10 @@ interface Commands { params: Bidi.Script.CallFunctionParameters; returnType: Bidi.Script.CallFunctionResult; }; + 'script.disown': { + params: Bidi.Script.DisownParameters; + returnType: Bidi.Script.DisownResult; + }; 'browsingContext.create': { params: Bidi.BrowsingContext.CreateParameters; returnType: Bidi.BrowsingContext.CreateResult; diff --git a/packages/puppeteer-core/src/common/bidi/JSHandle.ts b/packages/puppeteer-core/src/common/bidi/JSHandle.ts new file mode 100644 index 00000000000..1ae8ad50c77 --- /dev/null +++ b/packages/puppeteer-core/src/common/bidi/JSHandle.ts @@ -0,0 +1,146 @@ +/** + * 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 {ElementHandle} from '../../api/ElementHandle.js'; +import {EvaluateFuncWith, HandleFor, HandleOr} from '../../common/types.js'; +import {releaseReference} from './utils.js'; +import {Page} from './Page.js'; +import {JSHandle as BaseJSHandle} from '../../api/JSHandle.js'; +import {BidiSerializer} from './Serializer.js'; +import {Connection} from './Connection.js'; +import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; + +export class JSHandle extends BaseJSHandle { + #disposed = false; + #context; + #remoteValue; + + constructor(context: Page, remoteValue: Bidi.CommonDataTypes.RemoteValue) { + super(); + this.#context = context; + this.#remoteValue = remoteValue; + } + + context(): Page { + return this.#context; + } + + get connecton(): Connection { + return this.#context.connection; + } + + override get disposed(): boolean { + return this.#disposed; + } + + override async evaluate< + Params extends unknown[], + Func extends EvaluateFuncWith = EvaluateFuncWith + >( + pageFunction: Func | string, + ...args: Params + ): Promise>> { + return await this.context().evaluate(pageFunction, this, ...args); + } + + override async evaluateHandle< + Params extends unknown[], + Func extends EvaluateFuncWith = EvaluateFuncWith + >( + pageFunction: Func | string, + ...args: Params + ): Promise>>> { + return await this.context().evaluateHandle(pageFunction, this, ...args); + } + + override async getProperty( + propertyName: HandleOr + ): Promise>; + override async getProperty(propertyName: string): Promise>; + override async getProperty( + propertyName: HandleOr + ): Promise> { + return await this.evaluateHandle((object, propertyName) => { + return object[propertyName as K]; + }, propertyName); + } + + override async getProperties(): Promise> { + // TODO(lightning00blade): Either include return of depth Handles in RemoteValue + // or new BiDi command that returns array of remote value + const keys = await this.evaluate(object => { + return Object.getOwnPropertyNames(object); + }); + const map: Map = new Map(); + const results = await Promise.all( + keys.map(key => { + return this.getProperty(key); + }) + ); + + for (const [key, value] of Object.entries(keys)) { + const handle = results[key as any]; + if (handle) { + map.set(value, handle); + } + } + + return map; + } + + override async jsonValue(): Promise { + if (!('handle' in this.#remoteValue)) { + return BidiSerializer.deserialize(this.#remoteValue); + } + const value = await this.evaluate(object => { + return object; + }); + if (value === undefined) { + throw new Error('Could not serialize referenced object'); + } + return value; + } + + override asElement(): ElementHandle | null { + return null; + } + + override async dispose(): Promise { + if (this.#disposed) { + return; + } + this.#disposed = true; + if ('handle' in this.#remoteValue) { + await releaseReference(this.connecton, this.#remoteValue); + } + } + + override toString(): string { + if (!('handle' in this.#remoteValue)) { + return 'JSHandle:' + BidiSerializer.deserialize(this.#remoteValue); + } + + return 'JSHandle@' + this.#remoteValue.type; + } + + override get id(): string | undefined { + return 'handle' in this.#remoteValue ? this.#remoteValue.handle : undefined; + } + + bidiObject(): Bidi.CommonDataTypes.RemoteValue { + return this.#remoteValue; + } +} diff --git a/packages/puppeteer-core/src/common/bidi/Page.ts b/packages/puppeteer-core/src/common/bidi/Page.ts index a4c4fe37449..dd82a701e48 100644 --- a/packages/puppeteer-core/src/common/bidi/Page.ts +++ b/packages/puppeteer-core/src/common/bidi/Page.ts @@ -16,28 +16,45 @@ import {Page as PageBase} from '../../api/Page.js'; import {Connection} from './Connection.js'; -import type {EvaluateFunc} from '../types.js'; +import type {EvaluateFunc, HandleFor} from '../types.js'; import {isString, stringifyFunction} from '../util.js'; import {BidiSerializer} from './Serializer.js'; +import {JSHandle} from './JSHandle.js'; +import {Reference} from './types.js'; + /** * @internal */ export class Page extends PageBase { #connection: Connection; - #contextId: string; + _contextId: string; constructor(connection: Connection, contextId: string) { super(); this.#connection = connection; - this.#contextId = contextId; + this._contextId = contextId; } override async close(): Promise { await this.#connection.send('browsingContext.close', { - context: this.#contextId, + context: this._contextId, }); } + get connection(): Connection { + return this.#connection; + } + + override async evaluateHandle< + Params extends unknown[], + Func extends EvaluateFunc = EvaluateFunc + >( + pageFunction: Func | string, + ...args: Params + ): Promise>>> { + return this.#evaluate(false, pageFunction, ...args); + } + override async evaluate< Params extends unknown[], Func extends EvaluateFunc = EvaluateFunc @@ -45,18 +62,52 @@ export class Page extends PageBase { pageFunction: Func | string, ...args: Params ): Promise>> { + return this.#evaluate(true, pageFunction, ...args); + } + + async #evaluate< + Params extends unknown[], + Func extends EvaluateFunc = EvaluateFunc + >( + returnByValue: true, + pageFunction: Func | string, + ...args: Params + ): Promise>>; + async #evaluate< + Params extends unknown[], + Func extends EvaluateFunc = EvaluateFunc + >( + returnByValue: false, + pageFunction: Func | string, + ...args: Params + ): Promise>>>; + async #evaluate< + Params extends unknown[], + Func extends EvaluateFunc = EvaluateFunc + >( + returnByValue: boolean, + pageFunction: Func | string, + ...args: Params + ): Promise>> | Awaited>> { let responsePromise; + const resultOwnership = returnByValue ? 'none' : 'root'; if (isString(pageFunction)) { responsePromise = this.#connection.send('script.evaluate', { expression: pageFunction, - target: {context: this.#contextId}, + target: {context: this._contextId}, + resultOwnership, awaitPromise: true, }); } else { responsePromise = this.#connection.send('script.callFunction', { functionDeclaration: stringifyFunction(pageFunction), - arguments: await Promise.all(args.map(BidiSerializer.serialize)), - target: {context: this.#contextId}, + arguments: await Promise.all( + args.map(arg => { + return BidiSerializer.serialize(arg, this); + }) + ), + target: {context: this._contextId}, + resultOwnership, awaitPromise: true, }); } @@ -67,8 +118,22 @@ export class Page extends PageBase { throw new Error(result.exceptionDetails.text); } - return BidiSerializer.deserialize(result.result) as Awaited< - ReturnType - >; + return returnByValue + ? BidiSerializer.deserialize(result.result) + : getBidiHandle(this, result.result as Reference); } } + +/** + * @internal + */ +export function getBidiHandle(context: Page, result: Reference): JSHandle { + // TODO: | ElementHandle + if ( + (result.type === 'node' || result.type === 'window') && + context._contextId + ) { + throw new Error('ElementHandle not implemented'); + } + return new JSHandle(context, result); +} diff --git a/packages/puppeteer-core/src/common/bidi/Serializer.ts b/packages/puppeteer-core/src/common/bidi/Serializer.ts index 4d717a7dca4..338d5bf18ee 100644 --- a/packages/puppeteer-core/src/common/bidi/Serializer.ts +++ b/packages/puppeteer-core/src/common/bidi/Serializer.ts @@ -1,5 +1,7 @@ import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; import {debugError, isDate, isPlainObject, isRegExp} from '../util.js'; +import {JSHandle} from './JSHandle.js'; +import {Page} from './Page.js'; /** * @internal @@ -46,6 +48,18 @@ export class BidiSerializer { value: parsedArray, }; } else if (isPlainObject(arg)) { + try { + JSON.stringify(arg); + } catch (error) { + if ( + error instanceof TypeError && + error.message.startsWith('Converting circular structure to JSON') + ) { + error.message += ' Recursive objects are not allowed.'; + } + throw error; + } + const parsedObject: Bidi.CommonDataTypes.MappingLocalValue = []; for (const key in arg) { parsedObject.push([ @@ -112,8 +126,24 @@ export class BidiSerializer { } } - static serialize(arg: unknown): Bidi.CommonDataTypes.LocalOrRemoteValue { + static serialize( + arg: unknown, + context: Page + ): Bidi.CommonDataTypes.LocalOrRemoteValue { // TODO: See use case of LazyArgs + const objectHandle = arg && arg instanceof JSHandle ? arg : null; + if (objectHandle) { + if (objectHandle.context() !== context) { + throw new Error( + 'JSHandles can be evaluated only in the context they were created!' + ); + } + if (objectHandle.disposed) { + throw new Error('JSHandle is disposed!'); + } + return objectHandle.bidiObject(); + } + return BidiSerializer.serializeRemoveValue(arg); } @@ -146,8 +176,8 @@ export class BidiSerializer { }); case 'set': // TODO: Check expected output when value is undefined - return result.value.reduce((acc: Set, value: unknown) => { - return acc.add(value); + return result.value.reduce((acc: Set, value) => { + return acc.add(BidiSerializer.deserializeLocalValue(value)); }, new Set()); case 'object': if (result.value) { @@ -202,7 +232,7 @@ export class BidiSerializer { return {key, value}; } - static deserialize(result: Bidi.CommonDataTypes.RemoteValue): unknown { + static deserialize(result: Bidi.CommonDataTypes.RemoteValue): any { if (!result) { debugError('Service did not produce a result.'); return undefined; diff --git a/packages/puppeteer-core/src/common/bidi/types.ts b/packages/puppeteer-core/src/common/bidi/types.ts new file mode 100644 index 00000000000..0b3e09ab24c --- /dev/null +++ b/packages/puppeteer-core/src/common/bidi/types.ts @@ -0,0 +1,6 @@ +import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; + +export type Reference = Extract< + Bidi.CommonDataTypes.RemoteValue, + Bidi.CommonDataTypes.RemoteReference +>; diff --git a/packages/puppeteer-core/src/common/bidi/utils.ts b/packages/puppeteer-core/src/common/bidi/utils.ts new file mode 100644 index 00000000000..412cb30a14a --- /dev/null +++ b/packages/puppeteer-core/src/common/bidi/utils.ts @@ -0,0 +1,45 @@ +/** + * 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 {debug} from '../Debug.js'; +import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; +import {Connection} from './Connection.js'; + +/** + * @internal + */ +export const debugError = debug('puppeteer:error'); +/** + * @internal + */ +export async function releaseReference( + client: Connection, + remoteReference: Bidi.CommonDataTypes.RemoteReference +): Promise { + if (!remoteReference.handle) { + return; + } + await client + .send('script.disown', { + target: {realm: '', context: ''}, // TODO: Populate + handles: [remoteReference.handle], + }) + .catch((error: any) => { + // Exceptions might happen in case of a page been navigated or closed. + // Swallow these since they are harmless and we don't leak anything in this case. + debugError(error); + }); +} diff --git a/test/TestExpectations.json b/test/TestExpectations.json index aadeb3de1d7..69cb0f1a76f 100644 --- a/test/TestExpectations.json +++ b/test/TestExpectations.json @@ -414,8 +414,8 @@ "expectations": ["SKIP"] }, { - "testIdPattern": "[jshandle.spec] JSHandle JSHandle.click should work", - "platforms": ["darwin", "linux", "win32"], + "testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.click should return Point data", + "platforms": ["darwin", "win32", "linux"], "parameters": ["firefox"], "expectations": ["FAIL"] }, @@ -1756,5 +1756,59 @@ "platforms": ["darwin", "linux", "win32"], "parameters": ["webDriverBiDi"], "expectations": ["SKIP"] + }, + { + "testIdPattern": "[jshandle.spec]", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[jshandle.spec] JSHandle Page.evaluateHandle should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[jshandle.spec] JSHandle Page.evaluateHandle should return the RemoteObject", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[jshandle.spec] JSHandle Page.evaluateHandle should use the same JS wrappers", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[jshandle.spec] JSHandle JSHandle.jsonValue should not work with dates", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[jshandle.spec] JSHandle JSHandle.asElement should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[jshandle.spec] JSHandle JSHandle.asElement should return ElementHandle for TextNodes", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[jshandle.spec] JSHandle JSHandle.toString should work for complicated objects", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[jshandle.spec] JSHandle JSHandle.toString should work with different subtypes", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] } ] diff --git a/test/src/elementhandle.spec.ts b/test/src/elementhandle.spec.ts index 5c3df4624d5..20c7b8f564d 100644 --- a/test/src/elementhandle.spec.ts +++ b/test/src/elementhandle.spec.ts @@ -21,6 +21,7 @@ import { getTestState, setupTestBrowserHooks, setupTestPageAndContextHooks, + shortWaitForArrayToHaveAtLeastNElements, } from './mocha-utils.js'; import {Puppeteer} from 'puppeteer'; @@ -173,7 +174,6 @@ describe('ElementHandle specs', function () { }); describe('ElementHandle.click', function () { - // See https://github.com/puppeteer/puppeteer/issues/7175 it('should work', async () => { const {page, server} = getTestState(); @@ -186,6 +186,40 @@ describe('ElementHandle specs', function () { }) ).toBe('Clicked'); }); + it('should return Point data', async () => { + const {page} = getTestState(); + + const clicks: Array<[x: number, y: number]> = []; + + await page.exposeFunction('reportClick', (x: number, y: number): void => { + clicks.push([x, y]); + }); + + await page.evaluate(() => { + document.body.style.padding = '0'; + document.body.style.margin = '0'; + document.body.innerHTML = ` +
+ `; + document.body.addEventListener('click', e => { + (window as any).reportClick(e.clientX, e.clientY); + }); + }); + + const divHandle = (await page.$('div'))!; + await divHandle.click(); + await divHandle.click({ + offset: { + x: 10, + y: 15, + }, + }); + await shortWaitForArrayToHaveAtLeastNElements(clicks, 2); + expect(clicks).toEqual([ + [45 + 60, 45 + 30], // margin + middle point offset + [30 + 10, 30 + 15], // margin + offset + ]); + }); it('should work for Shadow DOM v1', async () => { const {page, server} = getTestState(); @@ -273,6 +307,70 @@ describe('ElementHandle specs', function () { }); }); + describe('ElementHandle.clickablePoint', function () { + it('should work', async () => { + const {page} = getTestState(); + + await page.evaluate(() => { + document.body.style.padding = '0'; + document.body.style.margin = '0'; + document.body.innerHTML = ` +
+ `; + }); + await page.evaluate(async () => { + return new Promise(resolve => { + return window.requestAnimationFrame(resolve); + }); + }); + const divHandle = (await page.$('div'))!; + expect(await divHandle.clickablePoint()).toEqual({ + x: 45 + 60, // margin + middle point offset + y: 45 + 30, // margin + middle point offset + }); + expect( + await divHandle.clickablePoint({ + x: 10, + y: 15, + }) + ).toEqual({ + x: 30 + 10, // margin + offset + y: 30 + 15, // margin + offset + }); + }); + + it('should work for iframes', async () => { + const {page} = getTestState(); + await page.evaluate(() => { + document.body.style.padding = '10px'; + document.body.style.margin = '10px'; + document.body.innerHTML = ` + + `; + }); + await page.evaluate(async () => { + return new Promise(resolve => { + return window.requestAnimationFrame(resolve); + }); + }); + const frame = page.frames()[1]!; + const divHandle = (await frame.$('div'))!; + expect(await divHandle.clickablePoint()).toEqual({ + x: 20 + 45 + 60, // iframe pos + margin + middle point offset + y: 20 + 45 + 30, // iframe pos + margin + middle point offset + }); + expect( + await divHandle.clickablePoint({ + x: 10, + y: 15, + }) + ).toEqual({ + x: 20 + 30 + 10, // iframe pos + margin + offset + y: 20 + 30 + 15, // iframe pos + margin + offset + }); + }); + }); + describe('Element.waitForSelector', () => { it('should wait correctly with waitForSelector on an element', async () => { const {page} = getTestState(); diff --git a/test/src/jshandle.spec.ts b/test/src/jshandle.spec.ts index aa37bcde6a5..dc5cd0b1a21 100644 --- a/test/src/jshandle.spec.ts +++ b/test/src/jshandle.spec.ts @@ -19,7 +19,6 @@ import { getTestState, setupTestBrowserHooks, setupTestPageAndContextHooks, - shortWaitForArrayToHaveAtLeastNElements, } from './mocha-utils.js'; describe('JSHandle', function () { @@ -336,105 +335,4 @@ describe('JSHandle', function () { ); }); }); - - describe('JSHandle.clickablePoint', function () { - it('should work', async () => { - const {page} = getTestState(); - - await page.evaluate(() => { - document.body.style.padding = '0'; - document.body.style.margin = '0'; - document.body.innerHTML = ` -
- `; - }); - await page.evaluate(async () => { - return new Promise(resolve => { - return window.requestAnimationFrame(resolve); - }); - }); - const divHandle = (await page.$('div'))!; - expect(await divHandle.clickablePoint()).toEqual({ - x: 45 + 60, // margin + middle point offset - y: 45 + 30, // margin + middle point offset - }); - expect( - await divHandle.clickablePoint({ - x: 10, - y: 15, - }) - ).toEqual({ - x: 30 + 10, // margin + offset - y: 30 + 15, // margin + offset - }); - }); - - it('should work for iframes', async () => { - const {page} = getTestState(); - await page.evaluate(() => { - document.body.style.padding = '10px'; - document.body.style.margin = '10px'; - document.body.innerHTML = ` - - `; - }); - await page.evaluate(async () => { - return new Promise(resolve => { - return window.requestAnimationFrame(resolve); - }); - }); - const frame = page.frames()[1]!; - const divHandle = (await frame.$('div'))!; - expect(await divHandle.clickablePoint()).toEqual({ - x: 20 + 45 + 60, // iframe pos + margin + middle point offset - y: 20 + 45 + 30, // iframe pos + margin + middle point offset - }); - expect( - await divHandle.clickablePoint({ - x: 10, - y: 15, - }) - ).toEqual({ - x: 20 + 30 + 10, // iframe pos + margin + offset - y: 20 + 30 + 15, // iframe pos + margin + offset - }); - }); - }); - - describe('JSHandle.click', function () { - it('should work', async () => { - const {page} = getTestState(); - - const clicks: Array<[x: number, y: number]> = []; - - await page.exposeFunction('reportClick', (x: number, y: number): void => { - clicks.push([x, y]); - }); - - await page.evaluate(() => { - document.body.style.padding = '0'; - document.body.style.margin = '0'; - document.body.innerHTML = ` -
- `; - document.body.addEventListener('click', e => { - (window as any).reportClick(e.clientX, e.clientY); - }); - }); - - const divHandle = (await page.$('div'))!; - await divHandle.click(); - await divHandle.click({ - offset: { - x: 10, - y: 15, - }, - }); - await shortWaitForArrayToHaveAtLeastNElements(clicks, 2); - expect(clicks).toEqual([ - [45 + 60, 45 + 30], // margin + middle point offset - [30 + 10, 30 + 15], // margin + offset - ]); - }); - }); });