From 1282981c451a4a3d0e84fccdd526786c08d6b42d Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Fri, 14 Feb 2025 17:55:04 -0800 Subject: [PATCH] chore: contain remote objects to browser-specific code --- .../src/server/bidi/bidiExecutionContext.ts | 63 ++++++++++--------- .../src/server/bidi/bidiPage.ts | 13 +--- .../src/server/chromium/crExecutionContext.ts | 19 ++++-- .../src/server/chromium/crPage.ts | 15 ++--- packages/playwright-core/src/server/dom.ts | 12 ++-- .../src/server/electron/electron.ts | 6 +- .../src/server/firefox/ffExecutionContext.ts | 23 +++++-- .../src/server/firefox/ffPage.ts | 16 ++--- .../playwright-core/src/server/javascript.ts | 38 ++++++----- packages/playwright-core/src/server/page.ts | 2 +- .../src/server/webkit/wkExecutionContext.ts | 23 +++++-- .../src/server/webkit/wkPage.ts | 14 ++--- .../src/server/webkit/wkWorkers.ts | 4 +- 13 files changed, 136 insertions(+), 112 deletions(-) diff --git a/packages/playwright-core/src/server/bidi/bidiExecutionContext.ts b/packages/playwright-core/src/server/bidi/bidiExecutionContext.ts index aea4672196..a0ea401ace 100644 --- a/packages/playwright-core/src/server/bidi/bidiExecutionContext.ts +++ b/packages/playwright-core/src/server/bidi/bidiExecutionContext.ts @@ -26,6 +26,7 @@ import type { BidiSession } from './bidiConnection'; export class BidiExecutionContext implements js.ExecutionContextDelegate { private readonly _session: BidiSession; readonly _target: bidi.Script.Target; + private _handleFactory!: js.HandleFactory; constructor(session: BidiSession, realmInfo: bidi.Script.RealmInfo) { this._session = session; @@ -42,6 +43,10 @@ export class BidiExecutionContext implements js.ExecutionContextDelegate { } } + setHandleFactory(handleFactory: js.HandleFactory) { + this._handleFactory = handleFactory; + } + async rawEvaluateJSON(expression: string): Promise { const response = await this._session.send('script.evaluate', { expression, @@ -98,37 +103,26 @@ export class BidiExecutionContext implements js.ExecutionContextDelegate { if (response.type === 'success') { if (returnByValue) return parseEvaluationResultValue(BidiDeserializer.deserialize(response.result)); - const objectId = 'handle' in response.result ? response.result.handle : undefined ; - return utilityScript._context.createHandle({ objectId, ...response.result }); + return this._createHandle(response.result); } throw new js.JavaScriptErrorInEvaluate('Unexpected response type: ' + JSON.stringify(response)); } - async getProperties(context: js.ExecutionContext, objectId: js.ObjectId): Promise> { - const handle = this.createHandle(context, { objectId }); - try { - const names = await handle.evaluate(object => { - const names = []; - const descriptors = Object.getOwnPropertyDescriptors(object); - for (const name in descriptors) { - if (descriptors[name]?.enumerable) - names.push(name); - } - return names; - }); - const values = await Promise.all(names.map(name => handle.evaluateHandle((object, name) => object[name], name))); - const map = new Map(); - for (let i = 0; i < names.length; i++) - map.set(names[i], values[i]); - return map; - } finally { - handle.dispose(); - } - } - - createHandle(context: js.ExecutionContext, jsRemoteObject: js.RemoteObject): js.JSHandle { - const remoteObject: bidi.Script.RemoteValue = jsRemoteObject as bidi.Script.RemoteValue; - return new js.JSHandle(context, remoteObject.type, renderPreview(remoteObject), jsRemoteObject.objectId, remoteObjectValue(remoteObject)); + async getProperties(context: js.ExecutionContext, handle: js.JSHandle): Promise> { + const names = await handle.evaluate(object => { + const names = []; + const descriptors = Object.getOwnPropertyDescriptors(object); + for (const name in descriptors) { + if (descriptors[name]?.enumerable) + names.push(name); + } + return names; + }); + const values = await Promise.all(names.map(name => handle.evaluateHandle((object, name) => object[name], name))); + const map = new Map(); + for (let i = 0; i < names.length; i++) + map.set(names[i], values[i]); + return map; } async releaseHandle(objectId: js.ObjectId): Promise { @@ -149,11 +143,11 @@ export class BidiExecutionContext implements js.ExecutionContextDelegate { }; } - async remoteObjectForNodeId(nodeId: bidi.Script.SharedReference): Promise { + async remoteObjectForNodeId(nodeId: bidi.Script.SharedReference): Promise { const result = await this._remoteValueForReference(nodeId); - if ('handle' in result) - return { objectId: result.handle!, ...result }; - throw new Error('Can\'t get remote object for nodeId'); + if (!('handle' in result)) + throw new Error('Can\'t get remote object for nodeId'); + return this._createHandle(result); } async contentFrameIdForFrame(handle: dom.ElementHandle) { @@ -192,6 +186,13 @@ export class BidiExecutionContext implements js.ExecutionContextDelegate { return response.result; throw new js.JavaScriptErrorInEvaluate('Unexpected response type: ' + JSON.stringify(response)); } + + _createHandle(remoteObject: bidi.Script.RemoteValue): js.JSHandle { + if (remoteObject.type === 'node') + return this._handleFactory.createElementHandle(remoteObject.handle!); + const objectId = 'handle' in remoteObject ? remoteObject.handle : undefined; + return this._handleFactory.createJSHandle(remoteObject.type, renderPreview(remoteObject), objectId, remoteObjectValue(remoteObject)); + } } function renderPreview(remoteObject: bidi.Script.RemoteValue): string | undefined { diff --git a/packages/playwright-core/src/server/bidi/bidiPage.ts b/packages/playwright-core/src/server/bidi/bidiPage.ts index db708f7ba3..f356cb0d2a 100644 --- a/packages/playwright-core/src/server/bidi/bidiPage.ts +++ b/packages/playwright-core/src/server/bidi/bidiPage.ts @@ -130,7 +130,6 @@ export class BidiPage implements PageDelegate { const frame = this._page._frameManager.frame(realmInfo.context); if (!frame) return; - const delegate = new BidiExecutionContext(this._session, realmInfo); let worldName: types.World; if (!realmInfo.sandbox) { worldName = 'main'; @@ -141,6 +140,7 @@ export class BidiPage implements PageDelegate { } else { return; } + const delegate = new BidiExecutionContext(this._session, realmInfo); const context = new dom.FrameExecutionContext(delegate, frame, worldName); (context as any)[contextDelegateSymbol] = delegate; frame._contextCreated(worldName, context); @@ -242,7 +242,7 @@ export class BidiPage implements PageDelegate { return; const callFrame = params.stackTrace?.callFrames[0]; const location = callFrame ?? { url: '', lineNumber: 1, columnNumber: 1 }; - this._page._addConsoleMessage(entry.method, entry.args.map(arg => context.createHandle({ objectId: (arg as any).handle, ...arg })), location, params.text || undefined); + this._page._addConsoleMessage(entry.method, entry.args.map(arg => toBidiExecutionContext(context)._createHandle(arg)), location, params.text || undefined); } async navigateFrame(frame: frames.Frame, url: string, referrer: string | undefined): Promise { @@ -431,10 +431,6 @@ export class BidiPage implements PageDelegate { return executionContext.frameIdForWindowHandle(windowHandle); } - isElementHandle(remoteObject: bidi.Script.RemoteValue): boolean { - return remoteObject.type === 'node'; - } - async getBoundingBox(handle: dom.ElementHandle): Promise { const box = await handle.evaluate(element => { if (!(element instanceof Element)) @@ -532,10 +528,7 @@ export class BidiPage implements PageDelegate { const fromContext = toBidiExecutionContext(handle._context); const nodeId = await fromContext.nodeIdForElementHandle(handle); const executionContext = toBidiExecutionContext(to); - const objectId = await executionContext.remoteObjectForNodeId(nodeId); - if (objectId) - return to.createHandle(objectId) as dom.ElementHandle; - throw new Error('Failed to adopt element handle.'); + return await executionContext.remoteObjectForNodeId(nodeId) as dom.ElementHandle; } async getAccessibilityTree(needle?: dom.ElementHandle): Promise<{tree: accessibility.AXNode, needle: accessibility.AXNode | null}> { diff --git a/packages/playwright-core/src/server/chromium/crExecutionContext.ts b/packages/playwright-core/src/server/chromium/crExecutionContext.ts index 910932caf3..b5a3a3d3a0 100644 --- a/packages/playwright-core/src/server/chromium/crExecutionContext.ts +++ b/packages/playwright-core/src/server/chromium/crExecutionContext.ts @@ -27,12 +27,17 @@ import type { Protocol } from './protocol'; export class CRExecutionContext implements js.ExecutionContextDelegate { _client: CRSession; _contextId: number; + private _handleFactory!: js.HandleFactory; constructor(client: CRSession, contextPayload: Protocol.Runtime.ExecutionContextDescription) { this._client = client; this._contextId = contextPayload.id; } + setHandleFactory(handleFactory: js.HandleFactory) { + this._handleFactory = handleFactory; + } + async rawEvaluateJSON(expression: string): Promise { const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.evaluate', { expression, @@ -69,25 +74,27 @@ export class CRExecutionContext implements js.ExecutionContextDelegate { }).catch(rewriteError); if (exceptionDetails) throw new js.JavaScriptErrorInEvaluate(getExceptionMessage(exceptionDetails)); - return returnByValue ? parseEvaluationResultValue(remoteObject.value) : utilityScript._context.createHandle(remoteObject); + return returnByValue ? parseEvaluationResultValue(remoteObject.value) : this._createHandle(remoteObject); } - async getProperties(context: js.ExecutionContext, objectId: js.ObjectId): Promise> { + async getProperties(context: js.ExecutionContext, object: js.JSHandle): Promise> { const response = await this._client.send('Runtime.getProperties', { - objectId, + objectId: object._objectId!, ownProperties: true }); const result = new Map(); for (const property of response.result) { if (!property.enumerable || !property.value) continue; - result.set(property.name, context.createHandle(property.value)); + result.set(property.name, this._createHandle(property.value)); } return result; } - createHandle(context: js.ExecutionContext, remoteObject: Protocol.Runtime.RemoteObject): js.JSHandle { - return new js.JSHandle(context, remoteObject.subtype || remoteObject.type, renderPreview(remoteObject), remoteObject.objectId, potentiallyUnserializableValue(remoteObject)); + _createHandle(remoteObject: Protocol.Runtime.RemoteObject): js.JSHandle { + if (remoteObject.subtype === 'node') + return this._handleFactory.createElementHandle(remoteObject.objectId!); + return this._handleFactory.createJSHandle(remoteObject.subtype || remoteObject.type, renderPreview(remoteObject), remoteObject.objectId, potentiallyUnserializableValue(remoteObject)); } async releaseHandle(objectId: js.ObjectId): Promise { diff --git a/packages/playwright-core/src/server/chromium/crPage.ts b/packages/playwright-core/src/server/chromium/crPage.ts index 7e873fa503..ec36bcfdc9 100644 --- a/packages/playwright-core/src/server/chromium/crPage.ts +++ b/packages/playwright-core/src/server/chromium/crPage.ts @@ -51,6 +51,7 @@ import type { InitScript, PageDelegate } from '../page'; import type { Progress } from '../progress'; import type * as types from '../types'; import type * as channels from '@protocol/channels'; +import type * as js from '../javascript'; const UTILITY_WORLD_NAME = '__playwright_utility_world__'; @@ -281,10 +282,6 @@ export class CRPage implements PageDelegate { return this._sessionForHandle(handle)._getOwnerFrame(handle); } - isElementHandle(remoteObject: any): boolean { - return (remoteObject as Protocol.Runtime.RemoteObject).subtype === 'node'; - } - async getBoundingBox(handle: dom.ElementHandle): Promise { return this._sessionForHandle(handle)._getBoundingBox(handle); } @@ -739,7 +736,7 @@ class FrameSession { session.on('Target.attachedToTarget', event => this._onAttachedToTarget(event)); session.on('Target.detachedFromTarget', event => this._onDetachedFromTarget(event)); session.on('Runtime.consoleAPICalled', event => { - const args = event.args.map(o => worker._existingExecutionContext!.createHandle(o)); + const args = event.args.map(o => toCRExecutionContext(worker._existingExecutionContext!)._createHandle(o)); this._page._addConsoleMessage(event.type, args, toConsoleMessageLocation(event.stackTrace)); }); session.on('Runtime.exceptionThrown', exception => this._page.emitOnContextOnceInitialized(BrowserContext.Events.PageError, exceptionToError(exception.exceptionDetails), this._page)); @@ -802,7 +799,7 @@ class FrameSession { const context = this._contextIdToContext.get(event.executionContextId); if (!context) return; - const values = event.args.map(arg => context.createHandle(arg)); + const values = event.args.map(arg => toCRExecutionContext(context)._createHandle(arg)); this._page._addConsoleMessage(event.type, values, toConsoleMessageLocation(event.stackTrace)); } @@ -1171,7 +1168,7 @@ class FrameSession { }); if (!result || result.object.subtype === 'null') throw new Error(dom.kUnableToAdoptErrorMessage); - return to.createHandle(result.object).asElement()!; + return toCRExecutionContext(to)._createHandle(result.object).asElement()!; } } @@ -1246,3 +1243,7 @@ function calculateUserAgentMetadata(options: types.BrowserContextOptions) { metadata.architecture = 'arm'; return metadata; } + +function toCRExecutionContext(executionContext: js.ExecutionContext): CRExecutionContext { + return executionContext._delegate as CRExecutionContext; +} diff --git a/packages/playwright-core/src/server/dom.ts b/packages/playwright-core/src/server/dom.ts index 2c93f113e4..2ad561f042 100644 --- a/packages/playwright-core/src/server/dom.ts +++ b/packages/playwright-core/src/server/dom.ts @@ -83,12 +83,6 @@ export class FrameExecutionContext extends js.ExecutionContext { return js.evaluateExpression(this, expression, { ...options, returnByValue: false }, arg); } - override createHandle(remoteObject: js.RemoteObject): js.JSHandle { - if (this.frame._page._delegate.isElementHandle(remoteObject)) - return new ElementHandle(this, remoteObject.objectId!); - return super.createHandle(remoteObject); - } - injectedScript(): Promise> { if (!this._injectedScriptPromise) { const custom: string[] = []; @@ -115,6 +109,10 @@ export class FrameExecutionContext extends js.ExecutionContext { } return this._injectedScriptPromise; } + + override createElementHandle(objectId: js.ObjectId): ElementHandle { + return new ElementHandle(this, objectId); + } } export class ElementHandle extends js.JSHandle { @@ -124,7 +122,7 @@ export class ElementHandle extends js.JSHandle { declare readonly _objectId: string; readonly _frame: frames.Frame; - constructor(context: FrameExecutionContext, objectId: string) { + constructor(context: FrameExecutionContext, objectId: js.ObjectId) { super(context, 'node', undefined, objectId); this._page = context.frame._page; this._frame = context.frame; diff --git a/packages/playwright-core/src/server/electron/electron.ts b/packages/playwright-core/src/server/electron/electron.ts index eb9d2f3e5c..ecff4853ce 100644 --- a/packages/playwright-core/src/server/electron/electron.ts +++ b/packages/playwright-core/src/server/electron/electron.ts @@ -116,7 +116,7 @@ export class ElectronApplication extends SdkObject { } if (!this._nodeExecutionContext) return; - const args = event.args.map(arg => this._nodeExecutionContext!.createHandle(arg)); + const args = event.args.map(arg => toCRExecutionContext(this._nodeExecutionContext!)._createHandle(arg)); const message = new ConsoleMessage(null, event.type, undefined, args, toConsoleMessageLocation(event.stackTrace)); this.emit(ElectronApplication.Events.Console, message); } @@ -151,6 +151,10 @@ export class ElectronApplication extends SdkObject { } } +function toCRExecutionContext(executionContext: js.ExecutionContext): CRExecutionContext { + return executionContext._delegate as CRExecutionContext; +} + export class Electron extends SdkObject { constructor(playwright: Playwright) { super(playwright, 'electron'); diff --git a/packages/playwright-core/src/server/firefox/ffExecutionContext.ts b/packages/playwright-core/src/server/firefox/ffExecutionContext.ts index 5de8cddae3..75a0e6e652 100644 --- a/packages/playwright-core/src/server/firefox/ffExecutionContext.ts +++ b/packages/playwright-core/src/server/firefox/ffExecutionContext.ts @@ -26,12 +26,17 @@ import type { Protocol } from './protocol'; export class FFExecutionContext implements js.ExecutionContextDelegate { _session: FFSession; _executionContextId: string; + private _handleFactory!: js.HandleFactory; constructor(session: FFSession, executionContextId: string) { this._session = session; this._executionContextId = executionContextId; } + setHandleFactory(handleFactory: js.HandleFactory) { + this._handleFactory = handleFactory; + } + async rawEvaluateJSON(expression: string): Promise { const payload = await this._session.send('Runtime.evaluate', { expression, @@ -66,22 +71,24 @@ export class FFExecutionContext implements js.ExecutionContextDelegate { checkException(payload.exceptionDetails); if (returnByValue) return parseEvaluationResultValue(payload.result!.value); - return utilityScript._context.createHandle(payload.result!); + return toFFExecutionContext(utilityScript._context)._createHandle(payload.result!); } - async getProperties(context: js.ExecutionContext, objectId: js.ObjectId): Promise> { + async getProperties(context: js.ExecutionContext, object: js.JSHandle): Promise> { const response = await this._session.send('Runtime.getObjectProperties', { executionContextId: this._executionContextId, - objectId, + objectId: object._objectId!, }); const result = new Map(); for (const property of response.properties) - result.set(property.name, context.createHandle(property.value)); + result.set(property.name, toFFExecutionContext(context)._createHandle(property.value)); return result; } - createHandle(context: js.ExecutionContext, remoteObject: Protocol.Runtime.RemoteObject): js.JSHandle { - return new js.JSHandle(context, remoteObject.subtype || remoteObject.type || '', renderPreview(remoteObject), remoteObject.objectId, potentiallyUnserializableValue(remoteObject)); + _createHandle(remoteObject: Protocol.Runtime.RemoteObject): js.JSHandle { + if (remoteObject.subtype === 'node') + return this._handleFactory.createElementHandle(remoteObject.objectId!); + return this._handleFactory.createJSHandle(remoteObject.subtype || remoteObject.type || '', renderPreview(remoteObject), remoteObject.objectId, potentiallyUnserializableValue(remoteObject)); } async releaseHandle(objectId: js.ObjectId): Promise { @@ -135,3 +142,7 @@ function renderPreview(object: Protocol.Runtime.RemoteObject): string | undefine if ('value' in object) return String(object.value); } + +export function toFFExecutionContext(executionContext: js.ExecutionContext): FFExecutionContext { + return executionContext._delegate as FFExecutionContext; +} diff --git a/packages/playwright-core/src/server/firefox/ffPage.ts b/packages/playwright-core/src/server/firefox/ffPage.ts index 75cfffd24e..e5f7198ef3 100644 --- a/packages/playwright-core/src/server/firefox/ffPage.ts +++ b/packages/playwright-core/src/server/firefox/ffPage.ts @@ -22,7 +22,7 @@ import { InitScript } from '../page'; import { Page, Worker } from '../page'; import { getAccessibilityTree } from './ffAccessibility'; import { FFSession } from './ffConnection'; -import { FFExecutionContext } from './ffExecutionContext'; +import { FFExecutionContext, toFFExecutionContext } from './ffExecutionContext'; import { RawKeyboardImpl, RawMouseImpl, RawTouchscreenImpl } from './ffInput'; import { FFNetworkManager } from './ffNetworkManager'; import { debugLogger } from '../utils/debugLogger'; @@ -234,7 +234,7 @@ export class FFPage implements PageDelegate { if (!context) return; // Juggler reports 'warn' for some internal messages generated by the browser. - this._page._addConsoleMessage(type === 'warn' ? 'warning' : type, args.map(arg => context.createHandle(arg)), location); + this._page._addConsoleMessage(type === 'warn' ? 'warning' : type, args.map(arg => toFFExecutionContext(context)._createHandle(arg)), location); } _onDialogOpened(params: Protocol.Page.dialogOpenedPayload) { @@ -262,7 +262,7 @@ export class FFPage implements PageDelegate { const context = this._contextIdToContext.get(executionContextId); if (!context) return; - const handle = context.createHandle(element).asElement()!; + const handle = toFFExecutionContext(context)._createHandle(element).asElement()!; await this._page._onFileChooserOpened(handle); } @@ -286,7 +286,7 @@ export class FFPage implements PageDelegate { workerSession.on('Runtime.console', event => { const { type, args, location } = event; const context = worker._existingExecutionContext!; - this._page._addConsoleMessage(type, args.map(arg => context.createHandle(arg)), location); + this._page._addConsoleMessage(type, args.map(arg => toFFExecutionContext(context)._createHandle(arg)), location); }); // Note: we receive worker exceptions directly from the page. } @@ -443,10 +443,6 @@ export class FFPage implements PageDelegate { return ownerFrameId || null; } - isElementHandle(remoteObject: any): boolean { - return remoteObject.subtype === 'node'; - } - async getBoundingBox(handle: dom.ElementHandle): Promise { const quads = await this.getContentQuads(handle); if (!quads || !quads.length) @@ -535,7 +531,7 @@ export class FFPage implements PageDelegate { }); if (!result.remoteObject) throw new Error(dom.kUnableToAdoptErrorMessage); - return to.createHandle(result.remoteObject) as dom.ElementHandle; + return toFFExecutionContext(to)._createHandle(result.remoteObject) as dom.ElementHandle; } async getAccessibilityTree(needle?: dom.ElementHandle) { @@ -564,7 +560,7 @@ export class FFPage implements PageDelegate { }); if (!result.remoteObject) throw new Error('Frame has been detached.'); - return context.createHandle(result.remoteObject) as dom.ElementHandle; + return toFFExecutionContext(context)._createHandle(result.remoteObject) as dom.ElementHandle; } shouldToggleStyleSheetToSyncAnimations(): boolean { diff --git a/packages/playwright-core/src/server/javascript.ts b/packages/playwright-core/src/server/javascript.ts index d8c781afe3..2019d424a6 100644 --- a/packages/playwright-core/src/server/javascript.ts +++ b/packages/playwright-core/src/server/javascript.ts @@ -24,10 +24,6 @@ import type * as dom from './dom'; import type { UtilityScript } from './injected/utilityScript'; export type ObjectId = string; -export type RemoteObject = { - objectId?: ObjectId, - value?: any -}; interface TaggedAsJSHandle { __jshandle: T; @@ -55,21 +51,27 @@ export interface ExecutionContextDelegate { rawEvaluateJSON(expression: string): Promise; rawEvaluateHandle(expression: string): Promise; evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: JSHandle, values: any[], objectIds: ObjectId[]): Promise; - getProperties(context: ExecutionContext, objectId: ObjectId): Promise>; - createHandle(context: ExecutionContext, remoteObject: RemoteObject): JSHandle; + getProperties(context: ExecutionContext, object: JSHandle): Promise>; releaseHandle(objectId: ObjectId): Promise; + setHandleFactory(factory: HandleFactory): void; } -export class ExecutionContext extends SdkObject { - private _delegate: ExecutionContextDelegate; +export interface HandleFactory { + createElementHandle(objectId: ObjectId): dom.ElementHandle; + createJSHandle(type: string, preview: string | undefined, objectId?: ObjectId, value?: any): JSHandle; +} + +export class ExecutionContext extends SdkObject implements HandleFactory { + _delegate: ExecutionContextDelegate; private _utilityScriptPromise: Promise | undefined; private _contextDestroyedScope = new LongStandingScope(); readonly worldNameForTest: string; - constructor(parent: SdkObject, delegate: ExecutionContextDelegate, worldNameForTest: string) { + constructor(parent: SdkObject, delegate: ExecutionContextDelegate, worldNameForTest: string, handleFactory?: HandleFactory) { super(parent, 'execution-context'); this.worldNameForTest = worldNameForTest; this._delegate = delegate; + delegate.setHandleFactory(this); } contextDestroyed(reason: string) { @@ -93,12 +95,8 @@ export class ExecutionContext extends SdkObject { return this._raceAgainstContextDestroyed(this._delegate.evaluateWithArguments(expression, returnByValue, utilityScript, values, objectIds)); } - getProperties(context: ExecutionContext, objectId: ObjectId): Promise> { - return this._raceAgainstContextDestroyed(this._delegate.getProperties(context, objectId)); - } - - createHandle(remoteObject: RemoteObject): JSHandle { - return this._delegate.createHandle(this, remoteObject); + getProperties(context: ExecutionContext, object: JSHandle): Promise> { + return this._raceAgainstContextDestroyed(this._delegate.getProperties(context, object)); } releaseHandle(objectId: ObjectId): Promise { @@ -125,6 +123,14 @@ export class ExecutionContext extends SdkObject { async doSlowMo() { // overridden in FrameExecutionContext } + + createElementHandle(objectId: string): dom.ElementHandle { + throw new Error('Not supported'); + } + + createJSHandle(type: string, preview: string | undefined, objectId?: ObjectId, value?: any): JSHandle { + return new JSHandle(this, type, preview, objectId, value); + } } export class JSHandle extends SdkObject { @@ -183,7 +189,7 @@ export class JSHandle extends SdkObject { async getProperties(): Promise> { if (!this._objectId) return new Map(); - return this._context.getProperties(this._context, this._objectId); + return this._context.getProperties(this._context, this); } rawValue() { diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts index f78222c67d..820c46f4aa 100644 --- a/packages/playwright-core/src/server/page.ts +++ b/packages/playwright-core/src/server/page.ts @@ -75,7 +75,6 @@ export interface PageDelegate { setBackgroundColor(color?: { r: number; g: number; b: number; a: number; }): Promise; takeScreenshot(progress: Progress, format: string, documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined, fitsViewport: boolean, scale: 'css' | 'device'): Promise; - isElementHandle(remoteObject: any): boolean; adoptElementHandle(handle: dom.ElementHandle, to: dom.FrameExecutionContext): Promise>; getContentFrame(handle: dom.ElementHandle): Promise; // Only called for frame owner elements. getOwnerFrame(handle: dom.ElementHandle): Promise; // Returns frameId. @@ -834,6 +833,7 @@ export class Worker extends SdkObject { _createExecutionContext(delegate: js.ExecutionContextDelegate) { this._existingExecutionContext = new js.ExecutionContext(this, delegate, 'worker'); this._executionContextCallback(this._existingExecutionContext); + return this._existingExecutionContext; } url(): string { diff --git a/packages/playwright-core/src/server/webkit/wkExecutionContext.ts b/packages/playwright-core/src/server/webkit/wkExecutionContext.ts index 2101b6002b..06ea3f523d 100644 --- a/packages/playwright-core/src/server/webkit/wkExecutionContext.ts +++ b/packages/playwright-core/src/server/webkit/wkExecutionContext.ts @@ -25,12 +25,17 @@ import type { WKSession } from './wkConnection'; export class WKExecutionContext implements js.ExecutionContextDelegate { private readonly _session: WKSession; readonly _contextId: number | undefined; + private _handleFactory!: js.HandleFactory; constructor(session: WKSession, contextId: number | undefined) { this._session = session; this._contextId = contextId; } + setHandleFactory(handleFactory: js.HandleFactory) { + this._handleFactory = handleFactory; + } + async rawEvaluateJSON(expression: string): Promise { try { const response = await this._session.send('Runtime.evaluate', { @@ -79,29 +84,31 @@ export class WKExecutionContext implements js.ExecutionContextDelegate { throw new js.JavaScriptErrorInEvaluate(response.result.description); if (returnByValue) return parseEvaluationResultValue(response.result.value); - return utilityScript._context.createHandle(response.result); + return toWKExecutionContext(utilityScript._context)._createHandle(response.result); } catch (error) { throw rewriteError(error); } } - async getProperties(context: js.ExecutionContext, objectId: js.ObjectId): Promise> { + async getProperties(context: js.ExecutionContext, object: js.JSHandle): Promise> { const response = await this._session.send('Runtime.getProperties', { - objectId, + objectId: object._objectId!, ownProperties: true }); const result = new Map(); for (const property of response.properties) { if (!property.enumerable || !property.value) continue; - result.set(property.name, context.createHandle(property.value)); + result.set(property.name, toWKExecutionContext(context)._createHandle(property.value)); } return result; } - createHandle(context: js.ExecutionContext, remoteObject: Protocol.Runtime.RemoteObject): js.JSHandle { + _createHandle(remoteObject: Protocol.Runtime.RemoteObject): js.JSHandle { + if (remoteObject.subtype === 'node') + return this._handleFactory.createElementHandle(remoteObject.objectId!); const isPromise = remoteObject.className === 'Promise'; - return new js.JSHandle(context, isPromise ? 'promise' : remoteObject.subtype || remoteObject.type, renderPreview(remoteObject), remoteObject.objectId, potentiallyUnserializableValue(remoteObject)); + return this._handleFactory.createJSHandle(isPromise ? 'promise' : remoteObject.subtype || remoteObject.type, renderPreview(remoteObject), remoteObject.objectId, potentiallyUnserializableValue(remoteObject)); } async releaseHandle(objectId: js.ObjectId): Promise { @@ -139,3 +146,7 @@ function renderPreview(object: Protocol.Runtime.RemoteObject): string | undefine return js.sparseArrayToString(object.preview.properties!); return object.description; } + +export function toWKExecutionContext(executionContext: js.ExecutionContext): WKExecutionContext { + return executionContext._delegate as WKExecutionContext; +} diff --git a/packages/playwright-core/src/server/webkit/wkPage.ts b/packages/playwright-core/src/server/webkit/wkPage.ts index eb8b536b29..0b7fb80b49 100644 --- a/packages/playwright-core/src/server/webkit/wkPage.ts +++ b/packages/playwright-core/src/server/webkit/wkPage.ts @@ -34,7 +34,7 @@ import { PageBinding } from '../page'; import { Page } from '../page'; import { getAccessibilityTree } from './wkAccessibility'; import { WKSession } from './wkConnection'; -import { WKExecutionContext } from './wkExecutionContext'; +import { toWKExecutionContext, WKExecutionContext } from './wkExecutionContext'; import { RawKeyboardImpl, RawMouseImpl, RawTouchscreenImpl } from './wkInput'; import { WKInterceptableRequest, WKRouteImpl } from './wkInterceptableRequest'; import { WKProvisionalPage } from './wkProvisionalPage'; @@ -562,7 +562,7 @@ export class WKPage implements PageDelegate { } if (!context) return; - handles.push(context.createHandle(p)); + handles.push(toWKExecutionContext(context)._createHandle(p)); } this._lastConsoleMessage = { derivedType, @@ -611,7 +611,7 @@ export class WKPage implements PageDelegate { let handle; try { const context = await this._page._frameManager.frame(event.frameId)!._mainContext(); - handle = context.createHandle(event.element).asElement()!; + handle = toWKExecutionContext(context)._createHandle(event.element).asElement()!; } catch (e) { // During async processing, frame/context may go away. We should not throw. return; @@ -869,10 +869,6 @@ export class WKPage implements PageDelegate { return nodeInfo.ownerFrameId || null; } - isElementHandle(remoteObject: any): boolean { - return (remoteObject as Protocol.Runtime.RemoteObject).subtype === 'node'; - } - async getBoundingBox(handle: dom.ElementHandle): Promise { const quads = await this.getContentQuads(handle); if (!quads || !quads.length) @@ -962,7 +958,7 @@ export class WKPage implements PageDelegate { }); if (!result || result.object.subtype === 'null') throw new Error(dom.kUnableToAdoptErrorMessage); - return to.createHandle(result.object) as dom.ElementHandle; + return toWKExecutionContext(to)._createHandle(result.object) as dom.ElementHandle; } async getAccessibilityTree(needle?: dom.ElementHandle): Promise<{tree: accessibility.AXNode, needle: accessibility.AXNode | null}> { @@ -986,7 +982,7 @@ export class WKPage implements PageDelegate { }); if (!result || result.object.subtype === 'null') throw new Error('Frame has been detached.'); - return context.createHandle(result.object) as dom.ElementHandle; + return toWKExecutionContext(context)._createHandle(result.object) as dom.ElementHandle; } private _maybeCancelCoopNavigationRequest(provisionalPage: WKProvisionalPage) { diff --git a/packages/playwright-core/src/server/webkit/wkWorkers.ts b/packages/playwright-core/src/server/webkit/wkWorkers.ts index 719d8df6be..8a094f34e6 100644 --- a/packages/playwright-core/src/server/webkit/wkWorkers.ts +++ b/packages/playwright-core/src/server/webkit/wkWorkers.ts @@ -17,7 +17,7 @@ import { eventsHelper } from '../utils/eventsHelper'; import { Worker } from '../page'; import { WKSession } from './wkConnection'; -import { WKExecutionContext } from './wkExecutionContext'; +import { toWKExecutionContext, WKExecutionContext } from './wkExecutionContext'; import type { Protocol } from './protocol'; import type { RegisteredListener } from '../utils/eventsHelper'; @@ -95,7 +95,7 @@ export class WKWorkers { derivedType = 'timeEnd'; const handles = (parameters || []).map(p => { - return worker._existingExecutionContext!.createHandle(p); + return toWKExecutionContext(worker._existingExecutionContext!)._createHandle(p); }); const location: types.ConsoleMessageLocation = { url: url || '',