diff --git a/packages/playwright-core/src/client/frame.ts b/packages/playwright-core/src/client/frame.ts index fc49e2ff45..69ea7e477d 100644 --- a/packages/playwright-core/src/client/frame.ts +++ b/packages/playwright-core/src/client/frame.ts @@ -187,6 +187,12 @@ export class Frame extends ChannelOwner implements api.Fr return parseResult(result.value); } + async _evaluateExposeUtilityScript(pageFunction: structs.PageFunction, arg?: Arg): Promise { + assertMaxArguments(arguments.length, 2); + const result = await this._channel.evaluateExpression({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', exposeUtilityScript: true, arg: serializeArgument(arg) }); + return parseResult(result.value); + } + async $(selector: string, options?: { strict?: boolean }): Promise | null> { const result = await this._channel.querySelector({ selector, ...options }); return ElementHandle.fromNullable(result.element) as ElementHandle | null; diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 8fc6afbd6d..5a5133206d 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -1295,6 +1295,7 @@ scheme.FrameDispatchEventResult = tOptional(tObject({})); scheme.FrameEvaluateExpressionParams = tObject({ expression: tString, isFunction: tOptional(tBoolean), + exposeUtilityScript: tOptional(tBoolean), arg: tType('SerializedArgument'), }); scheme.FrameEvaluateExpressionResult = tObject({ diff --git a/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts b/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts index 0646133dcc..8098f3c934 100644 --- a/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts @@ -76,7 +76,7 @@ export class FrameDispatcher extends Dispatcher { - return { value: serializeResult(await this._frame.evaluateExpressionAndWaitForSignals(params.expression, params.isFunction, parseArgument(params.arg), 'main')) }; + return { value: serializeResult(await this._frame.evaluateExpressionAndWaitForSignals(params.expression, { isFunction: params.isFunction, exposeUtilityScript: params.exposeUtilityScript }, parseArgument(params.arg), 'main')) }; } async evaluateExpressionHandle(params: channels.FrameEvaluateExpressionHandleParams, metadata: CallMetadata): Promise { diff --git a/packages/playwright-core/src/server/dom.ts b/packages/playwright-core/src/server/dom.ts index cb59808680..749c14c08b 100644 --- a/packages/playwright-core/src/server/dom.ts +++ b/packages/playwright-core/src/server/dom.ts @@ -71,19 +71,19 @@ export class FrameExecutionContext extends js.ExecutionContext { return js.evaluate(this, false /* returnByValue */, pageFunction, arg); } - async evaluateExpression(expression: string, isFunction: boolean | undefined, arg?: any): Promise { - return js.evaluateExpression(this, true /* returnByValue */, expression, isFunction, arg); + async evaluateExpression(expression: string, options: { isFunction?: boolean, exposeUtilityScript?: boolean }, arg?: any): Promise { + return js.evaluateExpression(this, expression, { ...options, returnByValue: true }, arg); } - async evaluateExpressionAndWaitForSignals(expression: string, isFunction: boolean | undefined, arg?: any): Promise { + async evaluateExpressionAndWaitForSignals(expression: string, options: { isFunction?: boolean, exposeUtilityScript?: boolean }, arg?: any): Promise { return await this.frame._page._frameManager.waitForSignalsCreatedBy(null, false /* noWaitFor */, async () => { - return this.evaluateExpression(expression, isFunction, arg); + return this.evaluateExpression(expression, options, arg); }); } - async evaluateExpressionHandleAndWaitForSignals(expression: string, isFunction: boolean | undefined, arg: any): Promise { + async evaluateExpressionHandleAndWaitForSignals(expression: string, options: { isFunction?: boolean, exposeUtilityScript?: boolean }, arg: any): Promise { return await this.frame._page._frameManager.waitForSignalsCreatedBy(null, false /* noWaitFor */, async () => { - return js.evaluateExpression(this, false /* returnByValue */, expression, isFunction, arg); + return js.evaluateExpression(this, expression, { ...options, returnByValue: false }, arg); }); } diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index 5dc10a2e46..7cab9f6cc3 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -595,12 +595,12 @@ export class Frame extends SdkObject { }); } - nonStallingEvaluateInExistingContext(expression: string, isFunction: boolean|undefined, world: types.World): Promise { + nonStallingEvaluateInExistingContext(expression: string, isFunction: boolean | undefined, world: types.World): Promise { return this.raceAgainstEvaluationStallingEvents(() => { const context = this._contextData.get(world)?.context; if (!context) throw new Error('Frame does not yet have the execution context'); - return context.evaluateExpression(expression, isFunction); + return context.evaluateExpression(expression, { isFunction }); }); } @@ -763,19 +763,19 @@ export class Frame extends SdkObject { async evaluateExpressionHandleAndWaitForSignals(expression: string, isFunction: boolean | undefined, arg: any, world: types.World = 'main'): Promise { const context = await this._context(world); - const handle = await context.evaluateExpressionHandleAndWaitForSignals(expression, isFunction, arg); + const handle = await context.evaluateExpressionHandleAndWaitForSignals(expression, { isFunction }, arg); return handle; } async evaluateExpression(expression: string, isFunction: boolean | undefined, arg: any, world: types.World = 'main'): Promise { const context = await this._context(world); - const value = await context.evaluateExpression(expression, isFunction, arg); + const value = await context.evaluateExpression(expression, { isFunction }, arg); return value; } - async evaluateExpressionAndWaitForSignals(expression: string, isFunction: boolean | undefined, arg: any, world: types.World = 'main'): Promise { + async evaluateExpressionAndWaitForSignals(expression: string, options: { isFunction?: boolean, exposeUtilityScript?: boolean }, arg: any, world: types.World = 'main'): Promise { const context = await this._context(world); - const value = await context.evaluateExpressionAndWaitForSignals(expression, isFunction, arg); + const value = await context.evaluateExpressionAndWaitForSignals(expression, options, arg); return value; } diff --git a/packages/playwright-core/src/server/injected/utilityScript.ts b/packages/playwright-core/src/server/injected/utilityScript.ts index 6da576cd08..815cf61219 100644 --- a/packages/playwright-core/src/server/injected/utilityScript.ts +++ b/packages/playwright-core/src/server/injected/utilityScript.ts @@ -17,10 +17,16 @@ import { serializeAsCallArgument, parseEvaluationResultValue } from '../isomorphic/utilityScriptSerializers'; export class UtilityScript { - evaluate(isFunction: boolean | undefined, returnByValue: boolean, expression: string, argCount: number, ...argsAndHandles: any[]) { + serializeAsCallArgument = serializeAsCallArgument; + parseEvaluationResultValue = parseEvaluationResultValue; + + evaluate(isFunction: boolean | undefined, returnByValue: boolean, exposeUtilityScript: boolean | undefined, expression: string, argCount: number, ...argsAndHandles: any[]) { const args = argsAndHandles.slice(0, argCount); const handles = argsAndHandles.slice(argCount); const parameters = args.map(a => parseEvaluationResultValue(a, handles)); + if (exposeUtilityScript) + parameters.unshift(this); + let result = globalThis.eval(expression); if (isFunction === true) { result = result(...parameters); diff --git a/packages/playwright-core/src/server/javascript.ts b/packages/playwright-core/src/server/javascript.ts index 768c89427d..46af3c5b93 100644 --- a/packages/playwright-core/src/server/javascript.ts +++ b/packages/playwright-core/src/server/javascript.ts @@ -226,12 +226,12 @@ export class JSHandle extends SdkObject { } export async function evaluate(context: ExecutionContext, returnByValue: boolean, pageFunction: Function | string, ...args: any[]): Promise { - return evaluateExpression(context, returnByValue, String(pageFunction), typeof pageFunction === 'function', ...args); + return evaluateExpression(context, String(pageFunction), { returnByValue, isFunction: typeof pageFunction === 'function' }, ...args); } -export async function evaluateExpression(context: ExecutionContext, returnByValue: boolean, expression: string, isFunction: boolean | undefined, ...args: any[]): Promise { +export async function evaluateExpression(context: ExecutionContext, expression: string, options: { returnByValue?: boolean, isFunction?: boolean, exposeUtilityScript?: boolean }, ...args: any[]): Promise { const utilityScript = await context.utilityScript(); - expression = normalizeEvaluationExpression(expression, isFunction); + expression = normalizeEvaluationExpression(expression, options.isFunction); const handles: (Promise)[] = []; const toDispose: Promise[] = []; const pushHandle = (handle: Promise): number => { @@ -262,18 +262,18 @@ export async function evaluateExpression(context: ExecutionContext, returnByValu } // See UtilityScript for arguments. - const utilityScriptValues = [isFunction, returnByValue, expression, args.length, ...args]; + const utilityScriptValues = [options.isFunction, options.returnByValue, options.exposeUtilityScript, expression, args.length, ...args]; const script = `(utilityScript, ...args) => utilityScript.evaluate(...args)`; try { - return await context.evaluateWithArguments(script, returnByValue, utilityScript, utilityScriptValues, utilityScriptObjectIds); + return await context.evaluateWithArguments(script, options.returnByValue || false, utilityScript, utilityScriptValues, utilityScriptObjectIds); } finally { toDispose.map(handlePromise => handlePromise.then(handle => handle.dispose())); } } -export async function evaluateExpressionAndWaitForSignals(context: ExecutionContext, returnByValue: boolean, expression: string, isFunction?: boolean, ...args: any[]): Promise { - return await context.waitForSignalsCreatedBy(() => evaluateExpression(context, returnByValue, expression, isFunction, ...args)); +export async function evaluateExpressionAndWaitForSignals(context: ExecutionContext, returnByValue: boolean, expression: string, isFunction: boolean | undefined, ...args: any[]): Promise { + return await context.waitForSignalsCreatedBy(() => evaluateExpression(context, expression, { returnByValue, isFunction }, ...args)); } export function parseUnserializableValue(unserializableValue: string): any { diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts index cbf176d008..f781bb8991 100644 --- a/packages/playwright-core/src/server/page.ts +++ b/packages/playwright-core/src/server/page.ts @@ -724,11 +724,11 @@ export class Worker extends SdkObject { } async evaluateExpression(expression: string, isFunction: boolean | undefined, arg: any): Promise { - return js.evaluateExpression(await this._executionContextPromise, true /* returnByValue */, expression, isFunction, arg); + return js.evaluateExpression(await this._executionContextPromise, expression, { returnByValue: true, isFunction }, arg); } async evaluateExpressionHandle(expression: string, isFunction: boolean | undefined, arg: any): Promise { - return js.evaluateExpression(await this._executionContextPromise, false /* returnByValue */, expression, isFunction, arg); + return js.evaluateExpression(await this._executionContextPromise, expression, { returnByValue: false, isFunction }, arg); } } diff --git a/packages/protocol/src/channels.ts b/packages/protocol/src/channels.ts index db08b9e779..6b451a99aa 100644 --- a/packages/protocol/src/channels.ts +++ b/packages/protocol/src/channels.ts @@ -2392,10 +2392,12 @@ export type FrameDispatchEventResult = void; export type FrameEvaluateExpressionParams = { expression: string, isFunction?: boolean, + exposeUtilityScript?: boolean, arg: SerializedArgument, }; export type FrameEvaluateExpressionOptions = { isFunction?: boolean, + exposeUtilityScript?: boolean, }; export type FrameEvaluateExpressionResult = { value: SerializedValue, diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 8ad5b4a795..b630f0d18a 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -1739,6 +1739,7 @@ Frame: parameters: expression: string isFunction: boolean? + exposeUtilityScript: boolean? arg: SerializedArgument returns: value: SerializedValue diff --git a/tests/page/page-evaluate.spec.ts b/tests/page/page-evaluate.spec.ts index 783f44ccbe..eee558ed90 100644 --- a/tests/page/page-evaluate.spec.ts +++ b/tests/page/page-evaluate.spec.ts @@ -702,3 +702,13 @@ it('should work with overridden globalThis.Window/Document/Node', async ({ page, }); } }); + +it('should expose utilityScript', async ({ page }) => { + const result = await (page.mainFrame() as any)._evaluateExposeUtilityScript((utilityScript, { a }) => { + return { utils: 'parseEvaluationResultValue' in utilityScript, a }; + }, { a: 42 }); + expect(result).toEqual({ + a: 42, + utils: true, + }); +});