chore: Implement JSHandle for BiDi (#9660)

This commit is contained in:
Nikolay Vitkov 2023-02-15 11:29:18 +01:00 committed by GitHub
parent 56f99f7b10
commit 0c85c0611c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 531 additions and 187 deletions

View File

@ -34,16 +34,16 @@ const windowHandle = await page.evaluateHandle(() => window);
## Methods ## Methods
| Method | Modifiers | Description | | Method | Modifiers | Description |
| ---------------------------------------------------------------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | ---------------------------------------------------------------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [asElement()](./puppeteer.jshandle.aselement.md) | | | | [asElement()](./puppeteer.jshandle.aselement.md) | | |
| [dispose()](./puppeteer.jshandle.dispose.md) | | Releases the object referenced by the handle for garbage collection. | | [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. | | [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. | | [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. | | [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.md) | | Fetches a single property from the referenced object. |
| [getProperty(propertyName)](./puppeteer.jshandle.getproperty_1.md) | | | | [getProperty(propertyName)](./puppeteer.jshandle.getproperty_1.md) | | |
| [getProperty(propertyName)](./puppeteer.jshandle.getproperty_2.md) | | | | [getProperty(propertyName)](./puppeteer.jshandle.getproperty_2.md) | | |
| [jsonValue()](./puppeteer.jshandle.jsonvalue.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) | | [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. | | [toString()](./puppeteer.jshandle.tostring.md) | | Returns a string representation of the JSHandle. |

View File

@ -4,7 +4,7 @@ sidebar_label: JSHandle.remoteObject
# JSHandle.remoteObject() method # 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: #### Signature:

View File

@ -177,9 +177,17 @@ export class JSHandle<T = unknown> {
throw new Error('Not implemented'); throw new Error('Not implemented');
} }
/**
* @internal
*/
get id(): string | undefined {
throw new Error('Not implemented');
}
/** /**
* Provides access to the * Provides access to the
* [Protocol.Runtime.RemoteObject](https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#type-RemoteObject) * [Protocol.Runtime.RemoteObject](https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#type-RemoteObject)
* backing this handle.
*/ */
remoteObject(): Protocol.Runtime.RemoteObject { remoteObject(): Protocol.Runtime.RemoteObject {
throw new Error('Not implemented'); throw new Error('Not implemented');

View File

@ -186,7 +186,7 @@ export class Accessibility {
let backendNodeId: number | undefined; let backendNodeId: number | undefined;
if (root) { if (root) {
const {node} = await this.#client.send('DOM.describeNode', { const {node} = await this.#client.send('DOM.describeNode', {
objectId: root.remoteObject().objectId, objectId: root.id,
}); });
backendNodeId = node.backendNodeId; backendNodeId = node.backendNodeId;
} }

View File

@ -32,7 +32,7 @@ const queryAXTree = async (
role?: string role?: string
): Promise<Protocol.Accessibility.AXNode[]> => { ): Promise<Protocol.Accessibility.AXNode[]> => {
const {nodes} = await client.send('Accessibility.queryAXTree', { const {nodes} = await client.send('Accessibility.queryAXTree', {
objectId: element.remoteObject().objectId, objectId: element.id,
accessibleName, accessibleName,
role, role,
}); });

View File

@ -43,12 +43,8 @@ import {
NodeFor, NodeFor,
} from './types.js'; } from './types.js';
import {KeyInput} from './USKeyboardLayout.js'; import {KeyInput} from './USKeyboardLayout.js';
import { import {debugError, isString} from './util.js';
debugError, import {CDPJSHandle} from './JSHandle.js';
isString,
releaseObject,
valueFromRemoteObject,
} from './util.js';
const applyOffsetsToQuad = ( const applyOffsetsToQuad = (
quad: Point[], quad: Point[],
@ -70,10 +66,8 @@ const applyOffsetsToQuad = (
export class CDPElementHandle< export class CDPElementHandle<
ElementType extends Node = Element ElementType extends Node = Element
> extends ElementHandle<ElementType> { > extends ElementHandle<ElementType> {
#disposed = false;
#frame: Frame; #frame: Frame;
#context: ExecutionContext; #jsHandle: CDPJSHandle<ElementType>;
#remoteObject: Protocol.Runtime.RemoteObject;
constructor( constructor(
context: ExecutionContext, context: ExecutionContext,
@ -81,8 +75,7 @@ export class CDPElementHandle<
frame: Frame frame: Frame
) { ) {
super(); super();
this.#context = context; this.#jsHandle = new CDPJSHandle(context, remoteObject);
this.#remoteObject = remoteObject;
this.#frame = frame; this.#frame = frame;
} }
@ -90,18 +83,22 @@ export class CDPElementHandle<
* @internal * @internal
*/ */
override executionContext(): ExecutionContext { override executionContext(): ExecutionContext {
return this.#context; return this.#jsHandle.executionContext();
} }
/** /**
* @internal * @internal
*/ */
override get client(): CDPSession { 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 { override remoteObject(): Protocol.Runtime.RemoteObject {
return this.#remoteObject; return this.#jsHandle.remoteObject();
} }
override async evaluate< override async evaluate<
@ -143,7 +140,7 @@ export class CDPElementHandle<
} }
override get disposed(): boolean { override get disposed(): boolean {
return this.#disposed; return this.#jsHandle.disposed;
} }
override async getProperty<K extends keyof ElementType>( override async getProperty<K extends keyof ElementType>(
@ -153,30 +150,27 @@ export class CDPElementHandle<
override async getProperty<K extends keyof ElementType>( override async getProperty<K extends keyof ElementType>(
propertyName: HandleOr<K> propertyName: HandleOr<K>
): Promise<HandleFor<ElementType[K]>> { ): Promise<HandleFor<ElementType[K]>> {
return this.evaluateHandle((object, propertyName) => { return this.#jsHandle.getProperty(propertyName);
return object[propertyName as K]; }
}, propertyName);
override async getProperties(): Promise<Map<string, JSHandle>> {
return this.#jsHandle.getProperties();
}
override asElement(): CDPElementHandle<ElementType> | null {
return this;
} }
override async jsonValue(): Promise<ElementType> { override async jsonValue(): Promise<ElementType> {
if (!this.#remoteObject.objectId) { return this.#jsHandle.jsonValue();
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 { override toString(): string {
if (!this.#remoteObject.objectId) { return this.#jsHandle.toString();
return 'JSHandle:' + valueFromRemoteObject(this.#remoteObject); }
}
const type = this.#remoteObject.subtype || this.#remoteObject.type; override async dispose(): Promise<void> {
return 'JSHandle@' + type; return await this.#jsHandle.dispose();
} }
override async $<Selector extends string>( override async $<Selector extends string>(
@ -297,10 +291,6 @@ export class CDPElementHandle<
return this as unknown as HandleFor<ElementFor<K>>; return this as unknown as HandleFor<ElementFor<K>>;
} }
override asElement(): CDPElementHandle<ElementType> | null {
return this;
}
override async contentFrame(): Promise<Frame | null> { override async contentFrame(): Promise<Frame | null> {
const nodeInfo = await this.client.send('DOM.describeNode', { const nodeInfo = await this.client.send('DOM.describeNode', {
objectId: this.remoteObject().objectId, objectId: this.remoteObject().objectId,
@ -464,7 +454,7 @@ export class CDPElementHandle<
#getBoxModel(): Promise<void | Protocol.DOM.GetBoxModelResponse> { #getBoxModel(): Promise<void | Protocol.DOM.GetBoxModelResponse> {
const params: Protocol.DOM.GetBoxModelRequest = { const params: Protocol.DOM.GetBoxModelRequest = {
objectId: this.remoteObject().objectId, objectId: this.id,
}; };
return this.client.send('DOM.getBoxModel', params).catch(error => { return this.client.send('DOM.getBoxModel', params).catch(error => {
return debugError(error); return debugError(error);
@ -846,14 +836,6 @@ export class CDPElementHandle<
return threshold === 1 ? visibleRatio === 1 : visibleRatio > threshold; return threshold === 1 ? visibleRatio === 1 : visibleRatio > threshold;
}, threshold); }, threshold);
} }
override async dispose(): Promise<void> {
if (this.#disposed) {
return;
}
this.#disposed = true;
await releaseObject(this.client, this.#remoteObject);
}
} }
function computeQuadArea(quad: Point[]): number { function computeQuadArea(quad: Point[]): number {

View File

@ -29,6 +29,8 @@ import {
stringifyFunction, stringifyFunction,
valueFromRemoteObject, valueFromRemoteObject,
} from './util.js'; } from './util.js';
import {CDPJSHandle} from './JSHandle.js';
import {CDPElementHandle} from './ElementHandle.js';
/** /**
* @public * @public
@ -321,7 +323,10 @@ export class ExecutionContext {
if (Object.is(arg, NaN)) { if (Object.is(arg, NaN)) {
return {unserializableValue: '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) {
if (objectHandle.executionContext() !== this) { if (objectHandle.executionContext() !== this) {
throw new Error( throw new Error(

View File

@ -539,7 +539,7 @@ export class IsolatedWorld {
'Cannot adopt handle that already belongs to this execution context' 'Cannot adopt handle that already belongs to this execution context'
); );
const nodeInfo = await this.#client.send('DOM.describeNode', { const nodeInfo = await this.#client.send('DOM.describeNode', {
objectId: handle.remoteObject().objectId, objectId: handle.id,
}); });
return (await this.adoptBackendNode(nodeInfo.node.backendNodeId)) as T; return (await this.adoptBackendNode(nodeInfo.node.backendNodeId)) as T;
} }

View File

@ -156,6 +156,10 @@ export class CDPJSHandle<T> extends JSHandle<T> {
return 'JSHandle@' + type; return 'JSHandle@' + type;
} }
override get id(): string | undefined {
return this.#remoteObject.objectId;
}
override remoteObject(): Protocol.Runtime.RemoteObject { override remoteObject(): Protocol.Runtime.RemoteObject {
return this.#remoteObject; return this.#remoteObject;
} }

View File

@ -525,13 +525,12 @@ export class CDPPage extends Page {
): Promise<JSHandle<Prototype[]>> { ): Promise<JSHandle<Prototype[]>> {
const context = await this.mainFrame().executionContext(); const context = await this.mainFrame().executionContext();
assert(!prototypeHandle.disposed, 'Prototype JSHandle is disposed!'); assert(!prototypeHandle.disposed, 'Prototype JSHandle is disposed!');
const remoteObject = prototypeHandle.remoteObject();
assert( assert(
remoteObject.objectId, prototypeHandle.id,
'Prototype JSHandle must not be referencing primitive value' 'Prototype JSHandle must not be referencing primitive value'
); );
const response = await context._client.send('Runtime.queryObjects', { const response = await context._client.send('Runtime.queryObjects', {
prototypeObjectId: remoteObject.objectId, prototypeObjectId: prototypeHandle.id,
}); });
return createJSHandle(context, response.objects) as HandleFor<Prototype[]>; return createJSHandle(context, response.objects) as HandleFor<Prototype[]>;
} }
@ -820,7 +819,7 @@ export class CDPPage extends Page {
} }
const textTokens = []; const textTokens = [];
for (const arg of args) { for (const arg of args) {
const remoteObject = arg.remoteObject(); const remoteObject = arg.remoteObject() as Protocol.Runtime.RemoteObject;
if (remoteObject.objectId) { if (remoteObject.objectId) {
textTokens.push(arg.toString()); textTokens.push(arg.toString());
} else { } else {

View File

@ -36,6 +36,10 @@ interface Commands {
params: Bidi.Script.CallFunctionParameters; params: Bidi.Script.CallFunctionParameters;
returnType: Bidi.Script.CallFunctionResult; returnType: Bidi.Script.CallFunctionResult;
}; };
'script.disown': {
params: Bidi.Script.DisownParameters;
returnType: Bidi.Script.DisownResult;
};
'browsingContext.create': { 'browsingContext.create': {
params: Bidi.BrowsingContext.CreateParameters; params: Bidi.BrowsingContext.CreateParameters;
returnType: Bidi.BrowsingContext.CreateResult; returnType: Bidi.BrowsingContext.CreateResult;

View File

@ -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<T = unknown> extends BaseJSHandle<T> {
#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<T, Params> = EvaluateFuncWith<T, Params>
>(
pageFunction: Func | string,
...args: Params
): Promise<Awaited<ReturnType<Func>>> {
return await this.context().evaluate(pageFunction, this, ...args);
}
override async evaluateHandle<
Params extends unknown[],
Func extends EvaluateFuncWith<T, Params> = EvaluateFuncWith<T, Params>
>(
pageFunction: Func | string,
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
return await this.context().evaluateHandle(pageFunction, this, ...args);
}
override async getProperty<K extends keyof T>(
propertyName: HandleOr<K>
): Promise<HandleFor<T[K]>>;
override async getProperty(propertyName: string): Promise<HandleFor<unknown>>;
override async getProperty<K extends keyof T>(
propertyName: HandleOr<K>
): Promise<HandleFor<T[K]>> {
return await this.evaluateHandle((object, propertyName) => {
return object[propertyName as K];
}, propertyName);
}
override async getProperties(): Promise<Map<string, BaseJSHandle>> {
// 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<string, BaseJSHandle> = 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<T> {
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<Node> | null {
return null;
}
override async dispose(): Promise<void> {
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;
}
}

View File

@ -16,28 +16,45 @@
import {Page as PageBase} from '../../api/Page.js'; import {Page as PageBase} from '../../api/Page.js';
import {Connection} from './Connection.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 {isString, stringifyFunction} from '../util.js';
import {BidiSerializer} from './Serializer.js'; import {BidiSerializer} from './Serializer.js';
import {JSHandle} from './JSHandle.js';
import {Reference} from './types.js';
/** /**
* @internal * @internal
*/ */
export class Page extends PageBase { export class Page extends PageBase {
#connection: Connection; #connection: Connection;
#contextId: string; _contextId: string;
constructor(connection: Connection, contextId: string) { constructor(connection: Connection, contextId: string) {
super(); super();
this.#connection = connection; this.#connection = connection;
this.#contextId = contextId; this._contextId = contextId;
} }
override async close(): Promise<void> { override async close(): Promise<void> {
await this.#connection.send('browsingContext.close', { 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<Params> = EvaluateFunc<Params>
>(
pageFunction: Func | string,
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
return this.#evaluate(false, pageFunction, ...args);
}
override async evaluate< override async evaluate<
Params extends unknown[], Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params> Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
@ -45,18 +62,52 @@ export class Page extends PageBase {
pageFunction: Func | string, pageFunction: Func | string,
...args: Params ...args: Params
): Promise<Awaited<ReturnType<Func>>> { ): Promise<Awaited<ReturnType<Func>>> {
return this.#evaluate(true, pageFunction, ...args);
}
async #evaluate<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
>(
returnByValue: true,
pageFunction: Func | string,
...args: Params
): Promise<Awaited<ReturnType<Func>>>;
async #evaluate<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
>(
returnByValue: false,
pageFunction: Func | string,
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>>;
async #evaluate<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
>(
returnByValue: boolean,
pageFunction: Func | string,
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>> | Awaited<ReturnType<Func>>> {
let responsePromise; let responsePromise;
const resultOwnership = returnByValue ? 'none' : 'root';
if (isString(pageFunction)) { if (isString(pageFunction)) {
responsePromise = this.#connection.send('script.evaluate', { responsePromise = this.#connection.send('script.evaluate', {
expression: pageFunction, expression: pageFunction,
target: {context: this.#contextId}, target: {context: this._contextId},
resultOwnership,
awaitPromise: true, awaitPromise: true,
}); });
} else { } else {
responsePromise = this.#connection.send('script.callFunction', { responsePromise = this.#connection.send('script.callFunction', {
functionDeclaration: stringifyFunction(pageFunction), functionDeclaration: stringifyFunction(pageFunction),
arguments: await Promise.all(args.map(BidiSerializer.serialize)), arguments: await Promise.all(
target: {context: this.#contextId}, args.map(arg => {
return BidiSerializer.serialize(arg, this);
})
),
target: {context: this._contextId},
resultOwnership,
awaitPromise: true, awaitPromise: true,
}); });
} }
@ -67,8 +118,22 @@ export class Page extends PageBase {
throw new Error(result.exceptionDetails.text); throw new Error(result.exceptionDetails.text);
} }
return BidiSerializer.deserialize(result.result) as Awaited< return returnByValue
ReturnType<Func> ? BidiSerializer.deserialize(result.result)
>; : getBidiHandle(this, result.result as Reference);
} }
} }
/**
* @internal
*/
export function getBidiHandle(context: Page, result: Reference): JSHandle {
// TODO: | ElementHandle<Node>
if (
(result.type === 'node' || result.type === 'window') &&
context._contextId
) {
throw new Error('ElementHandle not implemented');
}
return new JSHandle(context, result);
}

View File

@ -1,5 +1,7 @@
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import {debugError, isDate, isPlainObject, isRegExp} from '../util.js'; import {debugError, isDate, isPlainObject, isRegExp} from '../util.js';
import {JSHandle} from './JSHandle.js';
import {Page} from './Page.js';
/** /**
* @internal * @internal
@ -46,6 +48,18 @@ export class BidiSerializer {
value: parsedArray, value: parsedArray,
}; };
} else if (isPlainObject(arg)) { } 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 = []; const parsedObject: Bidi.CommonDataTypes.MappingLocalValue = [];
for (const key in arg) { for (const key in arg) {
parsedObject.push([ 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 // 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); return BidiSerializer.serializeRemoveValue(arg);
} }
@ -146,8 +176,8 @@ export class BidiSerializer {
}); });
case 'set': case 'set':
// TODO: Check expected output when value is undefined // TODO: Check expected output when value is undefined
return result.value.reduce((acc: Set<unknown>, value: unknown) => { return result.value.reduce((acc: Set<unknown>, value) => {
return acc.add(value); return acc.add(BidiSerializer.deserializeLocalValue(value));
}, new Set()); }, new Set());
case 'object': case 'object':
if (result.value) { if (result.value) {
@ -202,7 +232,7 @@ export class BidiSerializer {
return {key, value}; return {key, value};
} }
static deserialize(result: Bidi.CommonDataTypes.RemoteValue): unknown { static deserialize(result: Bidi.CommonDataTypes.RemoteValue): any {
if (!result) { if (!result) {
debugError('Service did not produce a result.'); debugError('Service did not produce a result.');
return undefined; return undefined;

View File

@ -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
>;

View File

@ -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<void> {
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);
});
}

View File

@ -414,8 +414,8 @@
"expectations": ["SKIP"] "expectations": ["SKIP"]
}, },
{ {
"testIdPattern": "[jshandle.spec] JSHandle JSHandle.click should work", "testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.click should return Point data",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "win32", "linux"],
"parameters": ["firefox"], "parameters": ["firefox"],
"expectations": ["FAIL"] "expectations": ["FAIL"]
}, },
@ -1756,5 +1756,59 @@
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],
"parameters": ["webDriverBiDi"], "parameters": ["webDriverBiDi"],
"expectations": ["SKIP"] "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"]
} }
] ]

View File

@ -21,6 +21,7 @@ import {
getTestState, getTestState,
setupTestBrowserHooks, setupTestBrowserHooks,
setupTestPageAndContextHooks, setupTestPageAndContextHooks,
shortWaitForArrayToHaveAtLeastNElements,
} from './mocha-utils.js'; } from './mocha-utils.js';
import {Puppeteer} from 'puppeteer'; import {Puppeteer} from 'puppeteer';
@ -173,7 +174,6 @@ describe('ElementHandle specs', function () {
}); });
describe('ElementHandle.click', function () { describe('ElementHandle.click', function () {
// See https://github.com/puppeteer/puppeteer/issues/7175
it('should work', async () => { it('should work', async () => {
const {page, server} = getTestState(); const {page, server} = getTestState();
@ -186,6 +186,40 @@ describe('ElementHandle specs', function () {
}) })
).toBe('Clicked'); ).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 = `
<div style="cursor: pointer; width: 120px; height: 60px; margin: 30px; padding: 15px;"></div>
`;
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 () => { it('should work for Shadow DOM v1', async () => {
const {page, server} = getTestState(); 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 = `
<div style="cursor: pointer; width: 120px; height: 60px; margin: 30px; padding: 15px;"></div>
`;
});
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 = `
<iframe style="border: none; margin: 0; padding: 0;" seamless sandbox srcdoc="<style>* { margin: 0; padding: 0;}</style><div style='cursor: pointer; width: 120px; height: 60px; margin: 30px; padding: 15px;' />"></iframe>
`;
});
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', () => { describe('Element.waitForSelector', () => {
it('should wait correctly with waitForSelector on an element', async () => { it('should wait correctly with waitForSelector on an element', async () => {
const {page} = getTestState(); const {page} = getTestState();

View File

@ -19,7 +19,6 @@ import {
getTestState, getTestState,
setupTestBrowserHooks, setupTestBrowserHooks,
setupTestPageAndContextHooks, setupTestPageAndContextHooks,
shortWaitForArrayToHaveAtLeastNElements,
} from './mocha-utils.js'; } from './mocha-utils.js';
describe('JSHandle', function () { 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 = `
<div style="cursor: pointer; width: 120px; height: 60px; margin: 30px; padding: 15px;"></div>
`;
});
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 = `
<iframe style="border: none; margin: 0; padding: 0;" seamless sandbox srcdoc="<style>* { margin: 0; padding: 0;}</style><div style='cursor: pointer; width: 120px; height: 60px; margin: 30px; padding: 15px;' />"></iframe>
`;
});
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 = `
<div style="cursor: pointer; width: 120px; height: 60px; margin: 30px; padding: 15px;"></div>
`;
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
]);
});
});
}); });