chore: unify evaluations across browsers even more (#2459)
This moves all the logic around UtilityScript to javascript.ts. Also uncovers a bug in WebKit where we cannot returnByValue after navigation.
This commit is contained in:
parent
1392dcd680
commit
d5c992e1db
|
|
@ -16,7 +16,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { CRSession } from './crConnection';
|
import { CRSession } from './crConnection';
|
||||||
import { helper } from '../helper';
|
|
||||||
import { getExceptionMessage, releaseObject } from './crProtocolHelper';
|
import { getExceptionMessage, releaseObject } from './crProtocolHelper';
|
||||||
import { Protocol } from './protocol';
|
import { Protocol } from './protocol';
|
||||||
import * as js from '../javascript';
|
import * as js from '../javascript';
|
||||||
|
|
@ -43,45 +42,22 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
|
||||||
return remoteObject.objectId!;
|
return remoteObject.objectId!;
|
||||||
}
|
}
|
||||||
|
|
||||||
async evaluate(context: js.ExecutionContext, returnByValue: boolean, pageFunction: Function | string, ...args: any[]): Promise<any> {
|
async evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: js.JSHandle<any>, values: any[], objectIds: string[]): Promise<any> {
|
||||||
if (helper.isString(pageFunction)) {
|
const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.callFunctionOn', {
|
||||||
return this._callOnUtilityScript(context,
|
functionDeclaration: expression,
|
||||||
`evaluate`, [
|
objectId: utilityScript._objectId,
|
||||||
{ value: debugSupport.ensureSourceUrl(pageFunction) },
|
arguments: [
|
||||||
], returnByValue, () => { });
|
{ objectId: utilityScript._objectId },
|
||||||
}
|
...values.map(value => ({ value })),
|
||||||
|
...objectIds.map(objectId => ({ objectId })),
|
||||||
if (typeof pageFunction !== 'function')
|
],
|
||||||
throw new Error(`Expected to get |string| or |function| as the first argument, but got "${pageFunction}" instead.`);
|
returnByValue,
|
||||||
const { functionText, values, handles, dispose } = await js.prepareFunctionCall(pageFunction, context, args);
|
awaitPromise: true,
|
||||||
return this._callOnUtilityScript(context,
|
userGesture: true
|
||||||
'callFunction', [
|
}).catch(rewriteError);
|
||||||
{ value: functionText },
|
if (exceptionDetails)
|
||||||
...values.map(value => ({ value })),
|
throw new Error('Evaluation failed: ' + getExceptionMessage(exceptionDetails));
|
||||||
...handles,
|
return returnByValue ? parseEvaluationResultValue(remoteObject.value) : utilityScript._context.createHandle(remoteObject);
|
||||||
], returnByValue, dispose);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _callOnUtilityScript(context: js.ExecutionContext, method: string, args: Protocol.Runtime.CallArgument[], returnByValue: boolean, dispose: () => void) {
|
|
||||||
try {
|
|
||||||
const utilityScript = await context.utilityScript();
|
|
||||||
const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.callFunctionOn', {
|
|
||||||
functionDeclaration: `function (...args) { return this.${method}(...args) }` + debugSupport.generateSourceUrl(),
|
|
||||||
objectId: utilityScript._objectId,
|
|
||||||
arguments: [
|
|
||||||
{ value: returnByValue },
|
|
||||||
...args
|
|
||||||
],
|
|
||||||
returnByValue,
|
|
||||||
awaitPromise: true,
|
|
||||||
userGesture: true
|
|
||||||
}).catch(rewriteError);
|
|
||||||
if (exceptionDetails)
|
|
||||||
throw new Error('Evaluation failed: ' + getExceptionMessage(exceptionDetails));
|
|
||||||
return returnByValue ? parseEvaluationResultValue(remoteObject.value) : context.createHandle(remoteObject);
|
|
||||||
} finally {
|
|
||||||
dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProperties(handle: js.JSHandle): Promise<Map<string, js.JSHandle>> {
|
async getProperties(handle: js.JSHandle): Promise<Map<string, js.JSHandle>> {
|
||||||
|
|
@ -110,16 +86,6 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
|
||||||
return;
|
return;
|
||||||
await releaseObject(this._client, handle._objectId);
|
await releaseObject(this._client, handle._objectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleJSONValue<T>(handle: js.JSHandle<T>): Promise<T> {
|
|
||||||
if (handle._objectId) {
|
|
||||||
return this._callOnUtilityScript(handle._context,
|
|
||||||
`jsonValue`, [
|
|
||||||
{ objectId: handle._objectId },
|
|
||||||
], true, () => {});
|
|
||||||
}
|
|
||||||
return handle._value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function rewriteError(error: Error): Protocol.Runtime.evaluateReturnValue {
|
function rewriteError(error: Error): Protocol.Runtime.evaluateReturnValue {
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ export class FrameExecutionContext extends js.ExecutionContext {
|
||||||
async evaluateInternal<Arg, R>(pageFunction: types.Func1<Arg, R>, arg: Arg): Promise<R>;
|
async evaluateInternal<Arg, R>(pageFunction: types.Func1<Arg, R>, arg: Arg): Promise<R>;
|
||||||
async evaluateInternal(pageFunction: never, ...args: never[]): Promise<any> {
|
async evaluateInternal(pageFunction: never, ...args: never[]): Promise<any> {
|
||||||
return await this.frame._page._frameManager.waitForSignalsCreatedBy(null, false /* noWaitFor */, async () => {
|
return await this.frame._page._frameManager.waitForSignalsCreatedBy(null, false /* noWaitFor */, async () => {
|
||||||
return this._delegate.evaluate(this, true /* returnByValue */, pageFunction, ...args);
|
return js.evaluate(this, true /* returnByValue */, pageFunction, ...args);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -72,7 +72,7 @@ export class FrameExecutionContext extends js.ExecutionContext {
|
||||||
async evaluateHandleInternal<Arg, R>(pageFunction: types.Func1<Arg, R>, arg: Arg): Promise<types.SmartHandle<R>>;
|
async evaluateHandleInternal<Arg, R>(pageFunction: types.Func1<Arg, R>, arg: Arg): Promise<types.SmartHandle<R>>;
|
||||||
async evaluateHandleInternal(pageFunction: never, ...args: never[]): Promise<any> {
|
async evaluateHandleInternal(pageFunction: never, ...args: never[]): Promise<any> {
|
||||||
return await this.frame._page._frameManager.waitForSignalsCreatedBy(null, false /* noWaitFor */, async () => {
|
return await this.frame._page._frameManager.waitForSignalsCreatedBy(null, false /* noWaitFor */, async () => {
|
||||||
return this._delegate.evaluate(this, false /* returnByValue */, pageFunction, ...args);
|
return js.evaluate(this, false /* returnByValue */, pageFunction, ...args);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { helper } from '../helper';
|
|
||||||
import * as js from '../javascript';
|
import * as js from '../javascript';
|
||||||
import { FFSession } from './ffConnection';
|
import { FFSession } from './ffConnection';
|
||||||
import { Protocol } from './protocol';
|
import { Protocol } from './protocol';
|
||||||
|
|
@ -42,46 +41,21 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
|
||||||
return payload.result!.objectId!;
|
return payload.result!.objectId!;
|
||||||
}
|
}
|
||||||
|
|
||||||
async evaluate(context: js.ExecutionContext, returnByValue: boolean, pageFunction: Function | string, ...args: any[]): Promise<any> {
|
async evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: js.JSHandle<any>, values: any[], objectIds: string[]): Promise<any> {
|
||||||
if (helper.isString(pageFunction)) {
|
const payload = await this._session.send('Runtime.callFunction', {
|
||||||
return this._callOnUtilityScript(context,
|
functionDeclaration: expression,
|
||||||
`evaluate`, [
|
args: [
|
||||||
{ value: debugSupport.ensureSourceUrl(pageFunction) },
|
{ objectId: utilityScript._objectId, value: undefined },
|
||||||
], returnByValue, () => {});
|
...values.map(value => ({ value })),
|
||||||
}
|
...objectIds.map(objectId => ({ objectId, value: undefined })),
|
||||||
if (typeof pageFunction !== 'function')
|
],
|
||||||
throw new Error(`Expected to get |string| or |function| as the first argument, but got "${pageFunction}" instead.`);
|
returnByValue,
|
||||||
|
executionContextId: this._executionContextId
|
||||||
const { functionText, values, handles, dispose } = await js.prepareFunctionCall(pageFunction, context, args);
|
}).catch(rewriteError);
|
||||||
|
checkException(payload.exceptionDetails);
|
||||||
return this._callOnUtilityScript(context,
|
if (returnByValue)
|
||||||
`callFunction`, [
|
return parseEvaluationResultValue(payload.result!.value);
|
||||||
{ value: functionText },
|
return utilityScript._context.createHandle(payload.result!);
|
||||||
...values.map(value => ({ value })),
|
|
||||||
...handles.map(handle => ({ objectId: handle.objectId, value: handle.value })),
|
|
||||||
], returnByValue, dispose);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _callOnUtilityScript(context: js.ExecutionContext, method: string, args: Protocol.Runtime.CallFunctionArgument[], returnByValue: boolean, dispose: () => void) {
|
|
||||||
try {
|
|
||||||
const utilityScript = await context.utilityScript();
|
|
||||||
const payload = await this._session.send('Runtime.callFunction', {
|
|
||||||
functionDeclaration: `(utilityScript, ...args) => utilityScript.${method}(...args)` + debugSupport.generateSourceUrl(),
|
|
||||||
args: [
|
|
||||||
{ objectId: utilityScript._objectId, value: undefined },
|
|
||||||
{ value: returnByValue },
|
|
||||||
...args
|
|
||||||
],
|
|
||||||
returnByValue,
|
|
||||||
executionContextId: this._executionContextId
|
|
||||||
}).catch(rewriteError);
|
|
||||||
checkException(payload.exceptionDetails);
|
|
||||||
if (returnByValue)
|
|
||||||
return parseEvaluationResultValue(payload.result!.value);
|
|
||||||
return context.createHandle(payload.result!);
|
|
||||||
} finally {
|
|
||||||
dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProperties(handle: js.JSHandle): Promise<Map<string, js.JSHandle>> {
|
async getProperties(handle: js.JSHandle): Promise<Map<string, js.JSHandle>> {
|
||||||
|
|
@ -110,16 +84,6 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
|
||||||
objectId: handle._objectId,
|
objectId: handle._objectId,
|
||||||
}).catch(error => {});
|
}).catch(error => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleJSONValue<T>(handle: js.JSHandle<T>): Promise<T> {
|
|
||||||
if (handle._objectId) {
|
|
||||||
return await this._callOnUtilityScript(handle._context,
|
|
||||||
`jsonValue`, [
|
|
||||||
{ objectId: handle._objectId, value: undefined },
|
|
||||||
], true, () => {});
|
|
||||||
}
|
|
||||||
return handle._value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkException(exceptionDetails?: Protocol.Runtime.ExceptionDetails) {
|
function checkException(exceptionDetails?: Protocol.Runtime.ExceptionDetails) {
|
||||||
|
|
|
||||||
|
|
@ -20,19 +20,20 @@ import * as utilityScriptSource from './generated/utilityScriptSource';
|
||||||
import { InnerLogger } from './logger';
|
import { InnerLogger } from './logger';
|
||||||
import * as debugSupport from './debug/debugSupport';
|
import * as debugSupport from './debug/debugSupport';
|
||||||
import { serializeAsCallArgument } from './utilityScriptSerializers';
|
import { serializeAsCallArgument } from './utilityScriptSerializers';
|
||||||
|
import { helper } from './helper';
|
||||||
|
|
||||||
|
type ObjectId = string;
|
||||||
export type RemoteObject = {
|
export type RemoteObject = {
|
||||||
objectId?: string,
|
objectId?: ObjectId,
|
||||||
value?: any
|
value?: any
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface ExecutionContextDelegate {
|
export interface ExecutionContextDelegate {
|
||||||
evaluate(context: ExecutionContext, returnByValue: boolean, pageFunction: string | Function, ...args: any[]): Promise<any>;
|
rawEvaluate(expression: string): Promise<ObjectId>;
|
||||||
rawEvaluate(pageFunction: string): Promise<string>;
|
evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: JSHandle<any>, values: any[], objectIds: ObjectId[]): Promise<any>;
|
||||||
getProperties(handle: JSHandle): Promise<Map<string, JSHandle>>;
|
getProperties(handle: JSHandle): Promise<Map<string, JSHandle>>;
|
||||||
createHandle(context: ExecutionContext, remoteObject: RemoteObject): JSHandle;
|
createHandle(context: ExecutionContext, remoteObject: RemoteObject): JSHandle;
|
||||||
releaseHandle(handle: JSHandle): Promise<void>;
|
releaseHandle(handle: JSHandle): Promise<void>;
|
||||||
handleJSONValue<T>(handle: JSHandle<T>): Promise<T>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ExecutionContext {
|
export class ExecutionContext {
|
||||||
|
|
@ -65,11 +66,11 @@ export class ExecutionContext {
|
||||||
export class JSHandle<T = any> {
|
export class JSHandle<T = any> {
|
||||||
readonly _context: ExecutionContext;
|
readonly _context: ExecutionContext;
|
||||||
_disposed = false;
|
_disposed = false;
|
||||||
readonly _objectId: string | undefined;
|
readonly _objectId: ObjectId | undefined;
|
||||||
readonly _value: any;
|
readonly _value: any;
|
||||||
private _objectType: string;
|
private _objectType: string;
|
||||||
|
|
||||||
constructor(context: ExecutionContext, type: string, objectId?: string, value?: any) {
|
constructor(context: ExecutionContext, type: string, objectId?: ObjectId, value?: any) {
|
||||||
this._context = context;
|
this._context = context;
|
||||||
this._objectId = objectId;
|
this._objectId = objectId;
|
||||||
this._value = value;
|
this._value = value;
|
||||||
|
|
@ -79,13 +80,13 @@ export class JSHandle<T = any> {
|
||||||
async evaluate<R, Arg>(pageFunction: types.FuncOn<T, Arg, R>, arg: Arg): Promise<R>;
|
async evaluate<R, Arg>(pageFunction: types.FuncOn<T, Arg, R>, arg: Arg): Promise<R>;
|
||||||
async evaluate<R>(pageFunction: types.FuncOn<T, void, R>, arg?: any): Promise<R>;
|
async evaluate<R>(pageFunction: types.FuncOn<T, void, R>, arg?: any): Promise<R>;
|
||||||
async evaluate<R, Arg>(pageFunction: types.FuncOn<T, Arg, R>, arg: Arg): Promise<R> {
|
async evaluate<R, Arg>(pageFunction: types.FuncOn<T, Arg, R>, arg: Arg): Promise<R> {
|
||||||
return this._context._delegate.evaluate(this._context, true /* returnByValue */, pageFunction, this, arg);
|
return evaluate(this._context, true /* returnByValue */, pageFunction, this, arg);
|
||||||
}
|
}
|
||||||
|
|
||||||
async evaluateHandle<R, Arg>(pageFunction: types.FuncOn<T, Arg, R>, arg: Arg): Promise<types.SmartHandle<R>>;
|
async evaluateHandle<R, Arg>(pageFunction: types.FuncOn<T, Arg, R>, arg: Arg): Promise<types.SmartHandle<R>>;
|
||||||
async evaluateHandle<R>(pageFunction: types.FuncOn<T, void, R>, arg?: any): Promise<types.SmartHandle<R>>;
|
async evaluateHandle<R>(pageFunction: types.FuncOn<T, void, R>, arg?: any): Promise<types.SmartHandle<R>>;
|
||||||
async evaluateHandle<R, Arg>(pageFunction: types.FuncOn<T, Arg, R>, arg: Arg): Promise<types.SmartHandle<R>> {
|
async evaluateHandle<R, Arg>(pageFunction: types.FuncOn<T, Arg, R>, arg: Arg): Promise<types.SmartHandle<R>> {
|
||||||
return this._context._delegate.evaluate(this._context, false /* returnByValue */, pageFunction, this, arg);
|
return evaluate(this._context, false /* returnByValue */, pageFunction, this, arg);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProperty(propertyName: string): Promise<JSHandle> {
|
async getProperty(propertyName: string): Promise<JSHandle> {
|
||||||
|
|
@ -104,8 +105,12 @@ export class JSHandle<T = any> {
|
||||||
return this._context._delegate.getProperties(this);
|
return this._context._delegate.getProperties(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
jsonValue(): Promise<T> {
|
async jsonValue(): Promise<T> {
|
||||||
return this._context._delegate.handleJSONValue(this);
|
if (!this._objectId)
|
||||||
|
return this._value;
|
||||||
|
const utilityScript = await this._context.utilityScript();
|
||||||
|
const script = `(utilityScript, ...args) => utilityScript.jsonValue(...args)` + debugSupport.generateSourceUrl();
|
||||||
|
return this._context._delegate.evaluateWithArguments(script, true, utilityScript, [true], [this._objectId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
asElement(): dom.ElementHandle | null {
|
asElement(): dom.ElementHandle | null {
|
||||||
|
|
@ -130,15 +135,14 @@ export class JSHandle<T = any> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type CallArgument = {
|
export async function evaluate(context: ExecutionContext, returnByValue: boolean, pageFunction: Function | string, ...args: any[]): Promise<any> {
|
||||||
value?: any,
|
const utilityScript = await context.utilityScript();
|
||||||
objectId?: string
|
if (helper.isString(pageFunction)) {
|
||||||
}
|
const script = `(utilityScript, ...args) => utilityScript.evaluate(...args)` + debugSupport.generateSourceUrl();
|
||||||
|
return context._delegate.evaluateWithArguments(script, returnByValue, utilityScript, [returnByValue, debugSupport.ensureSourceUrl(pageFunction)], []);
|
||||||
export async function prepareFunctionCall(
|
}
|
||||||
pageFunction: Function,
|
if (typeof pageFunction !== 'function')
|
||||||
context: ExecutionContext,
|
throw new Error(`Expected to get |string| or |function| as the first argument, but got "${pageFunction}" instead.`);
|
||||||
args: any[]): Promise<{ functionText: string, values: any[], handles: CallArgument[], dispose: () => void }> {
|
|
||||||
|
|
||||||
const originalText = pageFunction.toString();
|
const originalText = pageFunction.toString();
|
||||||
let functionText = originalText;
|
let functionText = originalText;
|
||||||
|
|
@ -180,18 +184,24 @@ export async function prepareFunctionCall(
|
||||||
}
|
}
|
||||||
return { fallThrough: handle };
|
return { fallThrough: handle };
|
||||||
}));
|
}));
|
||||||
const resultHandles: CallArgument[] = [];
|
|
||||||
|
const utilityScriptObjectIds: ObjectId[] = [];
|
||||||
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 Error('JSHandles can be evaluated only in the context they were created!');
|
throw new Error('JSHandles can be evaluated only in the context they were created!');
|
||||||
resultHandles.push({ objectId: handle._objectId });
|
utilityScriptObjectIds.push(handle._objectId!);
|
||||||
}
|
}
|
||||||
const dispose = () => {
|
|
||||||
toDispose.map(handlePromise => handlePromise.then(handle => handle.dispose()));
|
|
||||||
};
|
|
||||||
|
|
||||||
functionText += await debugSupport.generateSourceMapUrl(originalText, functionText);
|
functionText += await debugSupport.generateSourceMapUrl(originalText, functionText);
|
||||||
return { functionText, values: [ args.length, ...args ], handles: resultHandles, dispose };
|
// See UtilityScript for arguments.
|
||||||
|
const utilityScriptValues = [returnByValue, functionText, args.length, ...args];
|
||||||
|
|
||||||
|
const script = `(utilityScript, ...args) => utilityScript.callFunction(...args)` + debugSupport.generateSourceUrl();
|
||||||
|
try {
|
||||||
|
return context._delegate.evaluateWithArguments(script, returnByValue, utilityScript, utilityScriptValues, utilityScriptObjectIds);
|
||||||
|
} finally {
|
||||||
|
toDispose.map(handlePromise => handlePromise.then(handle => handle.dispose()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseUnserializableValue(unserializableValue: string): any {
|
export function parseUnserializableValue(unserializableValue: string): any {
|
||||||
|
|
|
||||||
|
|
@ -586,16 +586,14 @@ export class Worker extends EventEmitter {
|
||||||
async evaluate<R>(pageFunction: types.Func1<void, R>, arg?: any): Promise<R>;
|
async evaluate<R>(pageFunction: types.Func1<void, R>, arg?: any): Promise<R>;
|
||||||
async evaluate<R, Arg>(pageFunction: types.Func1<Arg, R>, arg: Arg): Promise<R> {
|
async evaluate<R, Arg>(pageFunction: types.Func1<Arg, R>, arg: Arg): Promise<R> {
|
||||||
assertMaxArguments(arguments.length, 2);
|
assertMaxArguments(arguments.length, 2);
|
||||||
const context = await this._executionContextPromise;
|
return js.evaluate(await this._executionContextPromise, true /* returnByValue */, pageFunction, arg);
|
||||||
return context._delegate.evaluate(context, true /* returnByValue */, pageFunction, arg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async evaluateHandle<R, Arg>(pageFunction: types.Func1<Arg, R>, arg: Arg): Promise<types.SmartHandle<R>>;
|
async evaluateHandle<R, Arg>(pageFunction: types.Func1<Arg, R>, arg: Arg): Promise<types.SmartHandle<R>>;
|
||||||
async evaluateHandle<R>(pageFunction: types.Func1<void, R>, arg?: any): Promise<types.SmartHandle<R>>;
|
async evaluateHandle<R>(pageFunction: types.Func1<void, R>, arg?: any): Promise<types.SmartHandle<R>>;
|
||||||
async evaluateHandle<R, Arg>(pageFunction: types.Func1<Arg, R>, arg: Arg): Promise<types.SmartHandle<R>> {
|
async evaluateHandle<R, Arg>(pageFunction: types.Func1<Arg, R>, arg: Arg): Promise<types.SmartHandle<R>> {
|
||||||
assertMaxArguments(arguments.length, 2);
|
assertMaxArguments(arguments.length, 2);
|
||||||
const context = await this._executionContextPromise;
|
return js.evaluate(await this._executionContextPromise, false /* returnByValue */, pageFunction, arg);
|
||||||
return context._delegate.evaluate(context, false /* returnByValue */, pageFunction, arg);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,7 @@ export class ElectronApplication extends ExtendedEventEmitter {
|
||||||
this._nodeExecutionContext = new js.ExecutionContext(new CRExecutionContext(this._nodeSession, event.context), this._logger);
|
this._nodeExecutionContext = new js.ExecutionContext(new CRExecutionContext(this._nodeSession, event.context), this._logger);
|
||||||
});
|
});
|
||||||
await this._nodeSession.send('Runtime.enable', {}).catch(e => {});
|
await this._nodeSession.send('Runtime.enable', {}).catch(e => {});
|
||||||
this._nodeElectronHandle = await this._nodeExecutionContext!._delegate.evaluate(this._nodeExecutionContext!, false /* returnByValue */, () => {
|
this._nodeElectronHandle = await js.evaluate(this._nodeExecutionContext!, false /* returnByValue */, () => {
|
||||||
// Resolving the race between the debugger and the boot-time script.
|
// Resolving the race between the debugger and the boot-time script.
|
||||||
if ((global as any)._playwrightRun)
|
if ((global as any)._playwrightRun)
|
||||||
return (global as any)._playwrightRun();
|
return (global as any)._playwrightRun();
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { WKSession, isSwappedOutError } from './wkConnection';
|
import { WKSession, isSwappedOutError } from './wkConnection';
|
||||||
import { helper } from '../helper';
|
|
||||||
import { Protocol } from './protocol';
|
import { Protocol } from './protocol';
|
||||||
import * as js from '../javascript';
|
import * as js from '../javascript';
|
||||||
import * as debugSupport from '../debug/debugSupport';
|
import * as debugSupport from '../debug/debugSupport';
|
||||||
|
|
@ -41,20 +40,33 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
async rawEvaluate(expression: string): Promise<string> {
|
async rawEvaluate(expression: string): Promise<string> {
|
||||||
const contextId = this._contextId;
|
try {
|
||||||
const response = await this._session.send('Runtime.evaluate', {
|
const response = await this._session.send('Runtime.evaluate', {
|
||||||
expression: debugSupport.ensureSourceUrl(expression),
|
expression: debugSupport.ensureSourceUrl(expression),
|
||||||
contextId,
|
contextId: this._contextId,
|
||||||
returnByValue: false
|
returnByValue: false
|
||||||
});
|
});
|
||||||
if (response.wasThrown)
|
if (response.wasThrown)
|
||||||
throw new Error('Evaluation failed: ' + response.result.description);
|
throw new Error('Evaluation failed: ' + response.result.description);
|
||||||
return response.result.objectId!;
|
return response.result.objectId!;
|
||||||
|
} catch (error) {
|
||||||
|
throw rewriteError(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async evaluate(context: js.ExecutionContext, returnByValue: boolean, pageFunction: Function | string, ...args: any[]): Promise<any> {
|
async evaluateWithArguments(expression: string, returnByValue: boolean, utilityScript: js.JSHandle<any>, values: any[], objectIds: string[]): Promise<any> {
|
||||||
try {
|
try {
|
||||||
let response = await this._evaluateRemoteObject(context, pageFunction, args, returnByValue);
|
let response = await this._session.send('Runtime.callFunctionOn', {
|
||||||
|
functionDeclaration: expression,
|
||||||
|
objectId: utilityScript._objectId!,
|
||||||
|
arguments: [
|
||||||
|
{ objectId: utilityScript._objectId },
|
||||||
|
...values.map(value => ({ value })),
|
||||||
|
...objectIds.map(objectId => ({ objectId })),
|
||||||
|
],
|
||||||
|
returnByValue: false, // We need to return real Promise if that is a promise.
|
||||||
|
emulateUserGesture: true
|
||||||
|
});
|
||||||
if (response.result.objectId && response.result.className === 'Promise') {
|
if (response.result.objectId && response.result.className === 'Promise') {
|
||||||
response = await Promise.race([
|
response = await Promise.race([
|
||||||
this._executionContextDestroyedPromise.then(() => contextDestroyedResult),
|
this._executionContextDestroyedPromise.then(() => contextDestroyedResult),
|
||||||
|
|
@ -67,53 +79,12 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
|
||||||
if (response.wasThrown)
|
if (response.wasThrown)
|
||||||
throw new Error('Evaluation failed: ' + response.result.description);
|
throw new Error('Evaluation failed: ' + response.result.description);
|
||||||
if (!returnByValue)
|
if (!returnByValue)
|
||||||
return context.createHandle(response.result);
|
return utilityScript._context.createHandle(response.result);
|
||||||
if (response.result.objectId)
|
if (response.result.objectId)
|
||||||
return await this._returnObjectByValue(context, response.result.objectId);
|
return await this._returnObjectByValue(utilityScript._context, response.result.objectId);
|
||||||
return parseEvaluationResultValue(response.result.value);
|
return parseEvaluationResultValue(response.result.value);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isSwappedOutError(error) || error.message.includes('Missing injected script for given'))
|
throw rewriteError(error);
|
||||||
throw new Error('Execution context was destroyed, most likely because of a navigation.');
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _evaluateRemoteObject(context: js.ExecutionContext, pageFunction: Function | string, args: any[], returnByValue: boolean): Promise<Protocol.Runtime.callFunctionOnReturnValue> {
|
|
||||||
if (helper.isString(pageFunction)) {
|
|
||||||
const utilityScript = await context.utilityScript();
|
|
||||||
const functionDeclaration = `function (returnByValue, pageFunction) { return this.evaluate(returnByValue, pageFunction); }` + debugSupport.generateSourceUrl();
|
|
||||||
return await this._session.send('Runtime.callFunctionOn', {
|
|
||||||
functionDeclaration,
|
|
||||||
objectId: utilityScript._objectId!,
|
|
||||||
arguments: [
|
|
||||||
{ value: returnByValue },
|
|
||||||
{ value: debugSupport.ensureSourceUrl(pageFunction) } ],
|
|
||||||
returnByValue: false, // We need to return real Promise if that is a promise.
|
|
||||||
emulateUserGesture: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof pageFunction !== 'function')
|
|
||||||
throw new Error(`Expected to get |string| or |function| as the first argument, but got "${pageFunction}" instead.`);
|
|
||||||
|
|
||||||
const { functionText, values, handles, dispose } = await js.prepareFunctionCall(pageFunction, context, args);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const utilityScript = await context.utilityScript();
|
|
||||||
return await this._session.send('Runtime.callFunctionOn', {
|
|
||||||
functionDeclaration: `function (...args) { return this.callFunction(...args) }` + debugSupport.generateSourceUrl(),
|
|
||||||
objectId: utilityScript._objectId!,
|
|
||||||
arguments: [
|
|
||||||
{ value: returnByValue },
|
|
||||||
{ value: functionText },
|
|
||||||
...values.map(value => ({ value })),
|
|
||||||
...handles,
|
|
||||||
],
|
|
||||||
returnByValue: false, // We need to return real Promise if that is a promise.
|
|
||||||
emulateUserGesture: true
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
dispose();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -130,10 +101,11 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
|
||||||
if (serializeResponse.wasThrown)
|
if (serializeResponse.wasThrown)
|
||||||
return undefined;
|
return undefined;
|
||||||
return parseEvaluationResultValue(serializeResponse.result.value);
|
return parseEvaluationResultValue(serializeResponse.result.value);
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
if (isSwappedOutError(e))
|
|
||||||
return contextDestroyedResult;
|
|
||||||
return undefined;
|
return undefined;
|
||||||
|
// TODO: we should actually throw an error, but that breaks the common case of undefined
|
||||||
|
// that is for some reason reported as an object and cannot be accessed after navigation.
|
||||||
|
// throw rewriteError(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -164,22 +136,6 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
|
||||||
return;
|
return;
|
||||||
await this._session.send('Runtime.releaseObject', {objectId: handle._objectId}).catch(error => {});
|
await this._session.send('Runtime.releaseObject', {objectId: handle._objectId}).catch(error => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleJSONValue<T>(handle: js.JSHandle<T>): Promise<T> {
|
|
||||||
if (handle._objectId) {
|
|
||||||
const utilityScript = await handle._context.utilityScript();
|
|
||||||
const response = await this._session.send('Runtime.callFunctionOn', {
|
|
||||||
functionDeclaration: 'function (object) { return this.jsonValue(true, object); }' + debugSupport.generateSourceUrl(),
|
|
||||||
objectId: utilityScript._objectId!,
|
|
||||||
arguments: [ { objectId: handle._objectId } ],
|
|
||||||
returnByValue: true
|
|
||||||
});
|
|
||||||
if (response.wasThrown)
|
|
||||||
throw new Error('Evaluation failed: ' + response.result.description);
|
|
||||||
return parseEvaluationResultValue(response.result.value);
|
|
||||||
}
|
|
||||||
return handle._value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const contextDestroyedResult = {
|
const contextDestroyedResult = {
|
||||||
|
|
@ -194,3 +150,9 @@ function potentiallyUnserializableValue(remoteObject: Protocol.Runtime.RemoteObj
|
||||||
const unserializableValue = remoteObject.type === 'number' && value === null ? remoteObject.description : undefined;
|
const unserializableValue = remoteObject.type === 'number' && value === null ? remoteObject.description : undefined;
|
||||||
return unserializableValue ? js.parseUnserializableValue(unserializableValue) : value;
|
return unserializableValue ? js.parseUnserializableValue(unserializableValue) : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function rewriteError(error: Error): Error {
|
||||||
|
if (isSwappedOutError(error) || error.message.includes('Missing injected script for given'))
|
||||||
|
return new Error('Execution context was destroyed, most likely because of a navigation.');
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -309,6 +309,22 @@ describe('Page.evaluate', function() {
|
||||||
});
|
});
|
||||||
expect(result).toEqual([42]);
|
expect(result).toEqual([42]);
|
||||||
});
|
});
|
||||||
|
it.fail(WEBKIT)('should not throw an error when evaluation does a synchronous navigation and returns an object', async({page, server}) => {
|
||||||
|
// It is imporant to be on about:blank for sync reload.
|
||||||
|
const result = await page.evaluate(() => {
|
||||||
|
window.location.reload();
|
||||||
|
return {a: 42};
|
||||||
|
});
|
||||||
|
expect(result).toEqual({a: 42});
|
||||||
|
});
|
||||||
|
it('should not throw an error when evaluation does a synchronous navigation and returns undefined', async({page, server}) => {
|
||||||
|
// It is imporant to be on about:blank for sync reload.
|
||||||
|
const result = await page.evaluate(() => {
|
||||||
|
window.location.reload();
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
expect(result).toBe(undefined);
|
||||||
|
});
|
||||||
it.slow()('should transfer 100Mb of data from page to node.js', async({page, server}) => {
|
it.slow()('should transfer 100Mb of data from page to node.js', async({page, server}) => {
|
||||||
const a = await page.evaluate(() => Array(100 * 1024 * 1024 + 1).join('a'));
|
const a = await page.evaluate(() => Array(100 * 1024 * 1024 + 1).join('a'));
|
||||||
expect(a.length).toBe(100 * 1024 * 1024);
|
expect(a.length).toBe(100 * 1024 * 1024);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue