Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 4 commits into from
Feb 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions packages/playwright-core/src/server/bidi/bidiExecutionContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export class BidiExecutionContext implements js.ExecutionContextDelegate {
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', {
expression,
target: this._target,
Expand All @@ -72,22 +72,22 @@ export class BidiExecutionContext implements js.ExecutionContextDelegate {
});
if (response.type === 'success') {
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));
}
if (response.type === 'exception')
throw new js.JavaScriptErrorInEvaluate(response.exceptionDetails.text + '\nFull val: ' + JSON.stringify(response.exceptionDetails));
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', {
functionDeclaration,
target: this._target,
arguments: [
{ handle: utilityScript._objectId! },
...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.
serializationOptions: returnByValue ? {} : { maxObjectDepth: 0, maxDomDepth: 0 },
Expand Down Expand Up @@ -121,10 +121,12 @@ export class BidiExecutionContext implements js.ExecutionContextDelegate {
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', {
target: this._target,
handles: [objectId],
handles: [handle._objectId],
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,24 +46,24 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
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', {
expression,
contextId: this._contextId,
}).catch(rewriteError);
if (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', {
functionDeclaration: expression,
objectId: utilityScript._objectId,
arguments: [
{ objectId: utilityScript._objectId },
...values.map(value => ({ value })),
...objectIds.map(objectId => ({ objectId })),
...handles.map(handle => ({ objectId: handle._objectId! })),
],
returnByValue,
awaitPromise: true,
Expand All @@ -88,8 +88,10 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
return result;
}

async releaseHandle(objectId: js.ObjectId): Promise<void> {
await releaseObject(this._client, objectId);
async releaseHandle(handle: js.JSHandle): Promise<void> {
if (!handle._objectId)
return;
await releaseObject(this._client, handle._objectId);
}
}

Expand Down
8 changes: 6 additions & 2 deletions packages/playwright-core/src/server/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -118,7 +122,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
declare readonly _objectId: string;
readonly _frame: frames.Frame;

constructor(context: FrameExecutionContext, objectId: js.ObjectId) {
constructor(context: FrameExecutionContext, objectId: string) {
super(context, 'node', undefined, objectId);
this._page = context.frame._page;
this._frame = context.frame;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,23 +44,23 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
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', {
expression,
returnByValue: false,
executionContextId: this._executionContextId,
}).catch(rewriteError);
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', {
functionDeclaration: expression,
args: [
{ objectId: utilityScript._objectId, value: undefined },
...values.map(value => ({ value })),
...objectIds.map(objectId => ({ objectId, value: undefined })),
...handles.map(handle => ({ objectId: handle._objectId!, value: undefined })),
],
returnByValue,
executionContextId: this._executionContextId
Expand All @@ -82,10 +82,12 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
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', {
executionContextId: this._executionContextId,
objectId
objectId: handle._objectId,
});
}
}
Expand Down
40 changes: 21 additions & 19 deletions packages/playwright-core/src/server/javascript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ import { LongStandingScope } from '../utils/isomorphic/manualPromise';
import type * as dom from './dom';
import type { UtilityScript } from './injected/utilityScript';

export type ObjectId = string;

interface TaggedAsJSHandle<T> {
__jshandle: T;
}
Expand All @@ -49,10 +47,10 @@ export type SmartHandle<T> = T extends Node ? dom.ElementHandle<T> : JSHandle<T>

export interface ExecutionContextDelegate {
rawEvaluateJSON(expression: string): Promise<any>;
rawEvaluateHandle(expression: string): Promise<ObjectId>;
evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: JSHandle<any>, values: any[], objectIds: ObjectId[]): Promise<any>;
rawEvaluateHandle(context: ExecutionContext, expression: string): Promise<JSHandle>;
evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: JSHandle, values: any[], handles: JSHandle[]): Promise<any>;
getProperties(object: JSHandle): Promise<Map<string, JSHandle>>;
releaseHandle(objectId: ObjectId): Promise<void>;
releaseHandle(handle: JSHandle): Promise<void>;
}

export class ExecutionContext extends SdkObject {
Expand All @@ -79,21 +77,21 @@ export class ExecutionContext extends SdkObject {
return this._raceAgainstContextDestroyed(this.delegate.rawEvaluateJSON(expression));
}

rawEvaluateHandle(expression: string): Promise<ObjectId> {
return this._raceAgainstContextDestroyed(this.delegate.rawEvaluateHandle(expression));
rawEvaluateHandle(expression: string): Promise<JSHandle> {
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();
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>> {
return this._raceAgainstContextDestroyed(this.delegate.getProperties(object));
}

releaseHandle(objectId: ObjectId): Promise<void> {
return this.delegate.releaseHandle(objectId);
releaseHandle(handle: JSHandle): Promise<void> {
return this.delegate.releaseHandle(handle);
}

adoptIfNeeded(handle: JSHandle): Promise<JSHandle> | null {
Expand All @@ -108,7 +106,11 @@ export class ExecutionContext extends SdkObject {
${utilityScriptSource.source}
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;
}
Expand All @@ -122,13 +124,13 @@ export class JSHandle<T = any> extends SdkObject {
__jshandle: T = true as any;
readonly _context: ExecutionContext;
_disposed = false;
readonly _objectId: ObjectId | undefined;
readonly _objectId: string | undefined;
readonly _value: any;
private _objectType: string;
protected _preview: string;
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');
this._context = context;
this._objectId = objectId;
Expand Down Expand Up @@ -185,7 +187,7 @@ export class JSHandle<T = any> extends SdkObject {
if (!this._objectId)
return this._value;
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 {
Expand All @@ -197,7 +199,7 @@ export class JSHandle<T = any> extends SdkObject {
return;
this._disposed = true;
if (this._objectId) {
this._context.releaseHandle(this._objectId).catch(e => {});
this._context.releaseHandle(this).catch(e => {});
if ((globalThis as any).leakedJSHandles)
(globalThis as any).leakedJSHandles.delete(this);
}
Expand Down Expand Up @@ -254,19 +256,19 @@ export async function evaluateExpression(context: ExecutionContext, expression:
return { fallThrough: handle };
}));

const utilityScriptObjectIds: ObjectId[] = [];
const utilityScriptObjects: JSHandle[] = [];
for (const handle of await Promise.all(handles)) {
if (handle._context !== context)
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.
const utilityScriptValues = [options.isFunction, options.returnByValue, expression, args.length, ...args];

const script = `(utilityScript, ...args) => utilityScript.evaluate(...args)`;
try {
return await context.evaluateWithArguments(script, options.returnByValue || false, utilityScriptValues, utilityScriptObjectIds);
return await context.evaluateWithArguments(script, options.returnByValue || false, utilityScriptValues, utilityScriptObjects);
} finally {
toDispose.map(handlePromise => handlePromise.then(handle => handle.dispose()));
}
Expand Down
14 changes: 8 additions & 6 deletions packages/playwright-core/src/server/webkit/wkExecutionContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
const response = await this._session.send('Runtime.evaluate', {
expression,
Expand All @@ -57,21 +57,21 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
});
if (response.wasThrown)
throw new js.JavaScriptErrorInEvaluate(response.result.description);
return response.result.objectId!;
return createHandle(context, response.result);
} catch (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 {
const response = await this._session.send('Runtime.callFunctionOn', {
functionDeclaration: expression,
objectId: utilityScript._objectId!,
arguments: [
{ objectId: utilityScript._objectId },
...values.map(value => ({ value })),
...objectIds.map(objectId => ({ objectId })),
...handles.map(handle => ({ objectId: handle._objectId! })),
],
returnByValue,
emulateUserGesture: true,
Expand Down Expand Up @@ -101,8 +101,10 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
return result;
}

async releaseHandle(objectId: js.ObjectId): Promise<void> {
await this._session.send('Runtime.releaseObject', { objectId });
async releaseHandle(handle: js.JSHandle): Promise<void> {
if (!handle._objectId)
return;
await this._session.send('Runtime.releaseObject', { objectId: handle._objectId });
}
}

Expand Down
Loading