chore: pass JSHandles instead of ObjectId to/from context delegate (#34895)

This commit is contained in:
Yury Semikhatsky 2025-02-24 12:11:17 -08:00 committed by GitHub
parent 954457ba9e
commit dbbdabfd1b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 59 additions and 45 deletions

View File

@ -61,7 +61,7 @@ export class BidiExecutionContext implements js.ExecutionContextDelegate {
throw new js.JavaScriptErrorInEvaluate('Unexpected response type: ' + JSON.stringify(response)); throw new js.JavaScriptErrorInEvaluate('Unexpected response type: ' + JSON.stringify(response));
} }
async rawEvaluateHandle(expression: string): Promise<js.ObjectId> { async rawEvaluateHandle(context: js.ExecutionContext, expression: string): Promise<js.JSHandle> {
const response = await this._session.send('script.evaluate', { const response = await this._session.send('script.evaluate', {
expression, expression,
target: this._target, target: this._target,
@ -72,7 +72,7 @@ export class BidiExecutionContext implements js.ExecutionContextDelegate {
}); });
if (response.type === 'success') { if (response.type === 'success') {
if ('handle' in response.result) if ('handle' in response.result)
return response.result.handle!; return createHandle(context, response.result);
throw new js.JavaScriptErrorInEvaluate('Cannot get handle: ' + JSON.stringify(response.result)); throw new js.JavaScriptErrorInEvaluate('Cannot get handle: ' + JSON.stringify(response.result));
} }
if (response.type === 'exception') if (response.type === 'exception')
@ -80,14 +80,14 @@ export class BidiExecutionContext implements js.ExecutionContextDelegate {
throw new js.JavaScriptErrorInEvaluate('Unexpected response type: ' + JSON.stringify(response)); throw new js.JavaScriptErrorInEvaluate('Unexpected response type: ' + JSON.stringify(response));
} }
async evaluateWithArguments(functionDeclaration: string, returnByValue: boolean, utilityScript: js.JSHandle<any>, values: any[], objectIds: string[]): Promise<any> { async evaluateWithArguments(functionDeclaration: string, returnByValue: boolean, utilityScript: js.JSHandle, values: any[], handles: js.JSHandle[]): Promise<any> {
const response = await this._session.send('script.callFunction', { const response = await this._session.send('script.callFunction', {
functionDeclaration, functionDeclaration,
target: this._target, target: this._target,
arguments: [ arguments: [
{ handle: utilityScript._objectId! }, { handle: utilityScript._objectId! },
...values.map(BidiSerializer.serialize), ...values.map(BidiSerializer.serialize),
...objectIds.map(handle => ({ handle })), ...handles.map(handle => ({ handle: handle._objectId! })),
], ],
resultOwnership: returnByValue ? undefined : bidi.Script.ResultOwnership.Root, // Necessary for the handle to be returned. resultOwnership: returnByValue ? undefined : bidi.Script.ResultOwnership.Root, // Necessary for the handle to be returned.
serializationOptions: returnByValue ? {} : { maxObjectDepth: 0, maxDomDepth: 0 }, serializationOptions: returnByValue ? {} : { maxObjectDepth: 0, maxDomDepth: 0 },
@ -121,10 +121,12 @@ export class BidiExecutionContext implements js.ExecutionContextDelegate {
return map; return map;
} }
async releaseHandle(objectId: js.ObjectId): Promise<void> { async releaseHandle(handle: js.JSHandle): Promise<void> {
if (!handle._objectId)
return;
await this._session.send('script.disown', { await this._session.send('script.disown', {
target: this._target, target: this._target,
handles: [objectId], handles: [handle._objectId],
}); });
} }

View File

@ -46,24 +46,24 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
return remoteObject.value; return remoteObject.value;
} }
async rawEvaluateHandle(expression: string): Promise<js.ObjectId> { async rawEvaluateHandle(context: js.ExecutionContext, expression: string): Promise<js.JSHandle> {
const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.evaluate', { const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.evaluate', {
expression, expression,
contextId: this._contextId, contextId: this._contextId,
}).catch(rewriteError); }).catch(rewriteError);
if (exceptionDetails) if (exceptionDetails)
throw new js.JavaScriptErrorInEvaluate(getExceptionMessage(exceptionDetails)); throw new js.JavaScriptErrorInEvaluate(getExceptionMessage(exceptionDetails));
return remoteObject.objectId!; return createHandle(context, remoteObject);
} }
async evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: js.JSHandle<any>, values: any[], objectIds: string[]): Promise<any> { async evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: js.JSHandle, values: any[], handles: js.JSHandle[]): Promise<any> {
const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.callFunctionOn', { const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.callFunctionOn', {
functionDeclaration: expression, functionDeclaration: expression,
objectId: utilityScript._objectId, objectId: utilityScript._objectId,
arguments: [ arguments: [
{ objectId: utilityScript._objectId }, { objectId: utilityScript._objectId },
...values.map(value => ({ value })), ...values.map(value => ({ value })),
...objectIds.map(objectId => ({ objectId })), ...handles.map(handle => ({ objectId: handle._objectId! })),
], ],
returnByValue, returnByValue,
awaitPromise: true, awaitPromise: true,
@ -88,8 +88,10 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
return result; return result;
} }
async releaseHandle(objectId: js.ObjectId): Promise<void> { async releaseHandle(handle: js.JSHandle): Promise<void> {
await releaseObject(this._client, objectId); if (!handle._objectId)
return;
await releaseObject(this._client, handle._objectId);
} }
} }

View File

@ -105,7 +105,11 @@ export class FrameExecutionContext extends js.ExecutionContext {
); );
})(); })();
`; `;
this._injectedScriptPromise = this.rawEvaluateHandle(source).then(objectId => new js.JSHandle(this, 'object', 'InjectedScript', objectId)); this._injectedScriptPromise = this.rawEvaluateHandle(source)
.then(handle => {
handle._setPreview('InjectedScript');
return handle;
});
} }
return this._injectedScriptPromise; return this._injectedScriptPromise;
} }
@ -118,7 +122,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
declare readonly _objectId: string; declare readonly _objectId: string;
readonly _frame: frames.Frame; readonly _frame: frames.Frame;
constructor(context: FrameExecutionContext, objectId: js.ObjectId) { constructor(context: FrameExecutionContext, objectId: string) {
super(context, 'node', undefined, objectId); super(context, 'node', undefined, objectId);
this._page = context.frame._page; this._page = context.frame._page;
this._frame = context.frame; this._frame = context.frame;

View File

@ -44,23 +44,23 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
return payload.result!.value; return payload.result!.value;
} }
async rawEvaluateHandle(expression: string): Promise<js.ObjectId> { async rawEvaluateHandle(context: js.ExecutionContext, expression: string): Promise<js.JSHandle> {
const payload = await this._session.send('Runtime.evaluate', { const payload = await this._session.send('Runtime.evaluate', {
expression, expression,
returnByValue: false, returnByValue: false,
executionContextId: this._executionContextId, executionContextId: this._executionContextId,
}).catch(rewriteError); }).catch(rewriteError);
checkException(payload.exceptionDetails); checkException(payload.exceptionDetails);
return payload.result!.objectId!; return createHandle(context, payload.result!);
} }
async evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: js.JSHandle<any>, values: any[], objectIds: string[]): Promise<any> { async evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: js.JSHandle, values: any[], handles: js.JSHandle[]): Promise<any> {
const payload = await this._session.send('Runtime.callFunction', { const payload = await this._session.send('Runtime.callFunction', {
functionDeclaration: expression, functionDeclaration: expression,
args: [ args: [
{ objectId: utilityScript._objectId, value: undefined }, { objectId: utilityScript._objectId, value: undefined },
...values.map(value => ({ value })), ...values.map(value => ({ value })),
...objectIds.map(objectId => ({ objectId, value: undefined })), ...handles.map(handle => ({ objectId: handle._objectId!, value: undefined })),
], ],
returnByValue, returnByValue,
executionContextId: this._executionContextId executionContextId: this._executionContextId
@ -82,10 +82,12 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
return result; return result;
} }
async releaseHandle(objectId: js.ObjectId): Promise<void> { async releaseHandle(handle: js.JSHandle): Promise<void> {
if (!handle._objectId)
return;
await this._session.send('Runtime.disposeObject', { await this._session.send('Runtime.disposeObject', {
executionContextId: this._executionContextId, executionContextId: this._executionContextId,
objectId objectId: handle._objectId,
}); });
} }
} }

View File

@ -23,8 +23,6 @@ import { LongStandingScope } from '../utils/isomorphic/manualPromise';
import type * as dom from './dom'; import type * as dom from './dom';
import type { UtilityScript } from './injected/utilityScript'; import type { UtilityScript } from './injected/utilityScript';
export type ObjectId = string;
interface TaggedAsJSHandle<T> { interface TaggedAsJSHandle<T> {
__jshandle: T; __jshandle: T;
} }
@ -49,10 +47,10 @@ export type SmartHandle<T> = T extends Node ? dom.ElementHandle<T> : JSHandle<T>
export interface ExecutionContextDelegate { export interface ExecutionContextDelegate {
rawEvaluateJSON(expression: string): Promise<any>; rawEvaluateJSON(expression: string): Promise<any>;
rawEvaluateHandle(expression: string): Promise<ObjectId>; rawEvaluateHandle(context: ExecutionContext, expression: string): Promise<JSHandle>;
evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: JSHandle<any>, values: any[], objectIds: ObjectId[]): Promise<any>; evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: JSHandle, values: any[], handles: JSHandle[]): Promise<any>;
getProperties(object: JSHandle): Promise<Map<string, JSHandle>>; getProperties(object: JSHandle): Promise<Map<string, JSHandle>>;
releaseHandle(objectId: ObjectId): Promise<void>; releaseHandle(handle: JSHandle): Promise<void>;
} }
export class ExecutionContext extends SdkObject { export class ExecutionContext extends SdkObject {
@ -79,21 +77,21 @@ export class ExecutionContext extends SdkObject {
return this._raceAgainstContextDestroyed(this.delegate.rawEvaluateJSON(expression)); return this._raceAgainstContextDestroyed(this.delegate.rawEvaluateJSON(expression));
} }
rawEvaluateHandle(expression: string): Promise<ObjectId> { rawEvaluateHandle(expression: string): Promise<JSHandle> {
return this._raceAgainstContextDestroyed(this.delegate.rawEvaluateHandle(expression)); return this._raceAgainstContextDestroyed(this.delegate.rawEvaluateHandle(this, expression));
} }
async evaluateWithArguments(expression: string, returnByValue: boolean, values: any[], objectIds: ObjectId[]): Promise<any> { async evaluateWithArguments(expression: string, returnByValue: boolean, values: any[], handles: JSHandle[]): Promise<any> {
const utilityScript = await this._utilityScript(); const utilityScript = await this._utilityScript();
return this._raceAgainstContextDestroyed(this.delegate.evaluateWithArguments(expression, returnByValue, utilityScript, values, objectIds)); return this._raceAgainstContextDestroyed(this.delegate.evaluateWithArguments(expression, returnByValue, utilityScript, values, handles));
} }
getProperties(object: JSHandle): Promise<Map<string, JSHandle>> { getProperties(object: JSHandle): Promise<Map<string, JSHandle>> {
return this._raceAgainstContextDestroyed(this.delegate.getProperties(object)); return this._raceAgainstContextDestroyed(this.delegate.getProperties(object));
} }
releaseHandle(objectId: ObjectId): Promise<void> { releaseHandle(handle: JSHandle): Promise<void> {
return this.delegate.releaseHandle(objectId); return this.delegate.releaseHandle(handle);
} }
adoptIfNeeded(handle: JSHandle): Promise<JSHandle> | null { adoptIfNeeded(handle: JSHandle): Promise<JSHandle> | null {
@ -108,7 +106,11 @@ export class ExecutionContext extends SdkObject {
${utilityScriptSource.source} ${utilityScriptSource.source}
return new (module.exports.UtilityScript())(${isUnderTest()}); return new (module.exports.UtilityScript())(${isUnderTest()});
})();`; })();`;
this._utilityScriptPromise = this._raceAgainstContextDestroyed(this.delegate.rawEvaluateHandle(source).then(objectId => new JSHandle(this, 'object', 'UtilityScript', objectId))); this._utilityScriptPromise = this._raceAgainstContextDestroyed(this.delegate.rawEvaluateHandle(this, source))
.then(handle => {
handle._setPreview('UtilityScript');
return handle;
});
} }
return this._utilityScriptPromise; return this._utilityScriptPromise;
} }
@ -122,13 +124,13 @@ export class JSHandle<T = any> extends SdkObject {
__jshandle: T = true as any; __jshandle: T = true as any;
readonly _context: ExecutionContext; readonly _context: ExecutionContext;
_disposed = false; _disposed = false;
readonly _objectId: ObjectId | undefined; readonly _objectId: string | undefined;
readonly _value: any; readonly _value: any;
private _objectType: string; private _objectType: string;
protected _preview: string; protected _preview: string;
private _previewCallback: ((preview: string) => void) | undefined; private _previewCallback: ((preview: string) => void) | undefined;
constructor(context: ExecutionContext, type: string, preview: string | undefined, objectId?: ObjectId, value?: any) { constructor(context: ExecutionContext, type: string, preview: string | undefined, objectId?: string, value?: any) {
super(context, 'handle'); super(context, 'handle');
this._context = context; this._context = context;
this._objectId = objectId; this._objectId = objectId;
@ -185,7 +187,7 @@ export class JSHandle<T = any> extends SdkObject {
if (!this._objectId) if (!this._objectId)
return this._value; return this._value;
const script = `(utilityScript, ...args) => utilityScript.jsonValue(...args)`; const script = `(utilityScript, ...args) => utilityScript.jsonValue(...args)`;
return this._context.evaluateWithArguments(script, true, [true], [this._objectId]); return this._context.evaluateWithArguments(script, true, [true], [this]);
} }
asElement(): dom.ElementHandle | null { asElement(): dom.ElementHandle | null {
@ -197,7 +199,7 @@ export class JSHandle<T = any> extends SdkObject {
return; return;
this._disposed = true; this._disposed = true;
if (this._objectId) { if (this._objectId) {
this._context.releaseHandle(this._objectId).catch(e => {}); this._context.releaseHandle(this).catch(e => {});
if ((globalThis as any).leakedJSHandles) if ((globalThis as any).leakedJSHandles)
(globalThis as any).leakedJSHandles.delete(this); (globalThis as any).leakedJSHandles.delete(this);
} }
@ -254,11 +256,11 @@ export async function evaluateExpression(context: ExecutionContext, expression:
return { fallThrough: handle }; return { fallThrough: handle };
})); }));
const utilityScriptObjectIds: ObjectId[] = []; const utilityScriptObjects: JSHandle[] = [];
for (const handle of await Promise.all(handles)) { for (const handle of await Promise.all(handles)) {
if (handle._context !== context) if (handle._context !== context)
throw new JavaScriptErrorInEvaluate('JSHandles can be evaluated only in the context they were created!'); throw new JavaScriptErrorInEvaluate('JSHandles can be evaluated only in the context they were created!');
utilityScriptObjectIds.push(handle._objectId!); utilityScriptObjects.push(handle);
} }
// See UtilityScript for arguments. // See UtilityScript for arguments.
@ -266,7 +268,7 @@ export async function evaluateExpression(context: ExecutionContext, expression:
const script = `(utilityScript, ...args) => utilityScript.evaluate(...args)`; const script = `(utilityScript, ...args) => utilityScript.evaluate(...args)`;
try { try {
return await context.evaluateWithArguments(script, options.returnByValue || false, utilityScriptValues, utilityScriptObjectIds); return await context.evaluateWithArguments(script, options.returnByValue || false, utilityScriptValues, utilityScriptObjects);
} finally { } finally {
toDispose.map(handlePromise => handlePromise.then(handle => handle.dispose())); toDispose.map(handlePromise => handlePromise.then(handle => handle.dispose()));
} }

View File

@ -48,7 +48,7 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
} }
} }
async rawEvaluateHandle(expression: string): Promise<js.ObjectId> { async rawEvaluateHandle(context: js.ExecutionContext, expression: string): Promise<js.JSHandle> {
try { try {
const response = await this._session.send('Runtime.evaluate', { const response = await this._session.send('Runtime.evaluate', {
expression, expression,
@ -57,13 +57,13 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
}); });
if (response.wasThrown) if (response.wasThrown)
throw new js.JavaScriptErrorInEvaluate(response.result.description); throw new js.JavaScriptErrorInEvaluate(response.result.description);
return response.result.objectId!; return createHandle(context, response.result);
} catch (error) { } catch (error) {
throw rewriteError(error); throw rewriteError(error);
} }
} }
async evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: js.JSHandle<any>, values: any[], objectIds: string[]): Promise<any> { async evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: js.JSHandle<any>, values: any[], handles: js.JSHandle[]): Promise<any> {
try { try {
const response = await this._session.send('Runtime.callFunctionOn', { const response = await this._session.send('Runtime.callFunctionOn', {
functionDeclaration: expression, functionDeclaration: expression,
@ -71,7 +71,7 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
arguments: [ arguments: [
{ objectId: utilityScript._objectId }, { objectId: utilityScript._objectId },
...values.map(value => ({ value })), ...values.map(value => ({ value })),
...objectIds.map(objectId => ({ objectId })), ...handles.map(handle => ({ objectId: handle._objectId! })),
], ],
returnByValue, returnByValue,
emulateUserGesture: true, emulateUserGesture: true,
@ -101,8 +101,10 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
return result; return result;
} }
async releaseHandle(objectId: js.ObjectId): Promise<void> { async releaseHandle(handle: js.JSHandle): Promise<void> {
await this._session.send('Runtime.releaseObject', { objectId }); if (!handle._objectId)
return;
await this._session.send('Runtime.releaseObject', { objectId: handle._objectId });
} }
} }