Skip to content

Commit

Permalink
chore: encapsulate ObjectId in browser-specific code
Browse files Browse the repository at this point in the history
  • Loading branch information
yury-s committed Feb 22, 2025
1 parent e091baa commit a0ec1c0
Show file tree
Hide file tree
Showing 6 changed files with 49 additions and 51 deletions.
16 changes: 8 additions & 8 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, handlePreview: 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, handlePreview);
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,10 @@ export class BidiExecutionContext implements js.ExecutionContextDelegate {
return map;
}

async releaseHandle(objectId: js.ObjectId): Promise<void> {
async releaseHandle(handle: js.JSHandle): Promise<void> {
await this._session.send('script.disown', {
target: this._target,
handles: [objectId],
handles: [handle._objectId!],
});
}

Expand Down Expand Up @@ -207,11 +207,11 @@ function remoteObjectValue(remoteObject: bidi.Script.RemoteValue): any {
return undefined;
}

export function createHandle(context: js.ExecutionContext, remoteObject: bidi.Script.RemoteValue): js.JSHandle {
export function createHandle(context: js.ExecutionContext, remoteObject: bidi.Script.RemoteValue, handlePreview?: string): js.JSHandle {
if (remoteObject.type === 'node') {
assert(context instanceof dom.FrameExecutionContext);
return new dom.ElementHandle(context, remoteObject.handle!);
}
const objectId = 'handle' in remoteObject ? remoteObject.handle : undefined;
return new js.JSHandle(context, remoteObject.type, renderPreview(remoteObject), objectId, remoteObjectValue(remoteObject));
return new js.JSHandle(context, remoteObject.type, handlePreview ?? renderPreview(remoteObject), objectId, remoteObjectValue(remoteObject));
}
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, handlePreview: 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, handlePreview);
}

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,8 @@ 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> {
await releaseObject(this._client, handle._objectId!);
}
}

Expand Down Expand Up @@ -131,10 +131,10 @@ function renderPreview(object: Protocol.Runtime.RemoteObject): string | undefine
return object.description;
}

export function createHandle(context: js.ExecutionContext, remoteObject: Protocol.Runtime.RemoteObject): js.JSHandle {
export function createHandle(context: js.ExecutionContext, remoteObject: Protocol.Runtime.RemoteObject, handlePreview?: string): js.JSHandle {
if (remoteObject.subtype === 'node') {
assert(context instanceof dom.FrameExecutionContext);
return new dom.ElementHandle(context, remoteObject.objectId!);
}
return new js.JSHandle(context, remoteObject.subtype || remoteObject.type, renderPreview(remoteObject), remoteObject.objectId, potentiallyUnserializableValue(remoteObject));
return new js.JSHandle(context, remoteObject.subtype || remoteObject.type, handlePreview ?? renderPreview(remoteObject), remoteObject.objectId, potentiallyUnserializableValue(remoteObject));
}
4 changes: 2 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,7 @@ 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, 'InjectedScript');
}
return this._injectedScriptPromise;
}
Expand All @@ -118,7 +118,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, handlePreview: 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!, handlePreview);
}

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,10 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
return result;
}

async releaseHandle(objectId: js.ObjectId): Promise<void> {
async releaseHandle(handle: js.JSHandle): Promise<void> {
await this._session.send('Runtime.disposeObject', {
executionContextId: this._executionContextId,
objectId
objectId: handle._objectId!,
});
}
}
Expand Down Expand Up @@ -134,10 +134,10 @@ function renderPreview(object: Protocol.Runtime.RemoteObject): string | undefine
return String(object.value);
}

export function createHandle(context: js.ExecutionContext, remoteObject: Protocol.Runtime.RemoteObject): js.JSHandle {
export function createHandle(context: js.ExecutionContext, remoteObject: Protocol.Runtime.RemoteObject, handlePreview?: string): js.JSHandle {
if (remoteObject.subtype === 'node') {
assert(context instanceof dom.FrameExecutionContext);
return new dom.ElementHandle(context, remoteObject.objectId!);
}
return new js.JSHandle(context, remoteObject.subtype || remoteObject.type || '', renderPreview(remoteObject), remoteObject.objectId, potentiallyUnserializableValue(remoteObject));
return new js.JSHandle(context, remoteObject.subtype || remoteObject.type || '', handlePreview ?? renderPreview(remoteObject), remoteObject.objectId, potentiallyUnserializableValue(remoteObject));
}
34 changes: 16 additions & 18 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, handlePreview: 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, preview: string): Promise<JSHandle> {
return this._raceAgainstContextDestroyed(this.delegate.rawEvaluateHandle(this, expression, preview));
}

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,7 @@ 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, 'UtilityScript'));
}
return this._utilityScriptPromise;
}
Expand All @@ -122,13 +120,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 +183,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 +195,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,11 +252,11 @@ export async function evaluateExpression(context: ExecutionContext, expression:
return { fallThrough: handle };
}));

const utilityScriptObjectIds: ObjectId[] = [];
const utilityScriptObjectIds: 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!);
utilityScriptObjectIds.push(handle);
}

// See UtilityScript for arguments.
Expand Down
14 changes: 7 additions & 7 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, handlePreview: 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, handlePreview);
} 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,8 @@ 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> {
await this._session.send('Runtime.releaseObject', { objectId: handle._objectId! });
}
}

Expand Down Expand Up @@ -137,7 +137,7 @@ function renderPreview(object: Protocol.Runtime.RemoteObject): string | undefine
return object.description;
}

export function createHandle(context: js.ExecutionContext, remoteObject: Protocol.Runtime.RemoteObject): js.JSHandle {
export function createHandle(context: js.ExecutionContext, remoteObject: Protocol.Runtime.RemoteObject, handlePreview?: string): js.JSHandle {
if (remoteObject.subtype === 'node') {
assert(context instanceof dom.FrameExecutionContext);
return new dom.ElementHandle(context as dom.FrameExecutionContext, remoteObject.objectId!);
Expand Down

0 comments on commit a0ec1c0

Please sign in to comment.