feat(evaluate): return user-readable error from evaluate (#2329)

This commit is contained in:
Pavel Feldman 2020-05-21 16:00:55 -07:00 committed by GitHub
parent 0a8fa6e46c
commit 5ee6494032
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 108 additions and 55 deletions

View file

@ -42,18 +42,10 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
async evaluate(context: js.ExecutionContext, returnByValue: boolean, pageFunction: Function | string, ...args: any[]): Promise<any> { async evaluate(context: js.ExecutionContext, returnByValue: boolean, pageFunction: Function | string, ...args: any[]): Promise<any> {
if (helper.isString(pageFunction)) { if (helper.isString(pageFunction)) {
const contextId = this._contextId; return this._callOnUtilityScript(context,
const expression: string = pageFunction; `evaluate`, [
const {exceptionDetails, result: remoteObject} = await this._client.send('Runtime.evaluate', { { value: js.ensureSourceUrl(pageFunction) },
expression: js.ensureSourceUrl(expression), ], returnByValue, () => { });
contextId,
returnByValue,
awaitPromise: true,
userGesture: true
}).catch(rewriteError);
if (exceptionDetails)
throw new Error('Evaluation failed: ' + getExceptionMessage(exceptionDetails));
return returnByValue ? valueFromRemoteObject(remoteObject) : context.createHandle(remoteObject);
} }
if (typeof pageFunction !== 'function') if (typeof pageFunction !== 'function')
@ -81,15 +73,23 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
return { value }; return { value };
}); });
try { return this._callOnUtilityScript(context,
const utilityScript = await context.utilityScript(); 'callFunction', [
const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.callFunctionOn', {
functionDeclaration: `function (...args) { return this.evaluate(...args) }${js.generateSourceUrl()}`,
objectId: utilityScript._remoteObject.objectId,
arguments: [
{ value: functionText }, { value: functionText },
...values.map(value => ({ value })), ...values.map(value => ({ value })),
...handles, ...handles,
], 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) }${js.generateSourceUrl()}`,
objectId: utilityScript._remoteObject.objectId,
arguments: [
{ value: returnByValue },
...args
], ],
returnByValue, returnByValue,
awaitPromise: true, awaitPromise: true,

View file

@ -41,15 +41,10 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
async evaluate(context: js.ExecutionContext, returnByValue: boolean, pageFunction: Function | string, ...args: any[]): Promise<any> { async evaluate(context: js.ExecutionContext, returnByValue: boolean, pageFunction: Function | string, ...args: any[]): Promise<any> {
if (helper.isString(pageFunction)) { if (helper.isString(pageFunction)) {
const payload = await this._session.send('Runtime.evaluate', { return this._callOnUtilityScript(context,
expression: js.ensureSourceUrl(pageFunction), `evaluate`, [
returnByValue, { value: pageFunction },
executionContextId: this._executionContextId, ], returnByValue, () => {});
}).catch(rewriteError);
checkException(payload.exceptionDetails);
if (returnByValue)
return deserializeValue(payload.result!);
return context.createHandle(payload.result);
} }
if (typeof pageFunction !== 'function') if (typeof pageFunction !== 'function')
throw new Error(`Expected to get |string| or |function| as the first argument, but got "${pageFunction}" instead.`); throw new Error(`Expected to get |string| or |function| as the first argument, but got "${pageFunction}" instead.`);
@ -68,15 +63,23 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
return { value }; return { value };
}); });
try { return this._callOnUtilityScript(context,
const utilityScript = await context.utilityScript(); `callFunction`, [
const payload = await this._session.send('Runtime.callFunction', {
functionDeclaration: `(utilityScript, ...args) => utilityScript.evaluate(...args)`,
args: [
{ objectId: utilityScript._remoteObject.objectId, value: undefined },
{ value: functionText }, { value: functionText },
...values.map(value => ({ value })), ...values.map(value => ({ value })),
...handles, ...handles,
], 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)`,
args: [
{ objectId: utilityScript._remoteObject.objectId, value: undefined },
{ value: returnByValue },
...args
], ],
returnByValue, returnByValue,
executionContextId: this._executionContextId executionContextId: this._executionContextId

View file

@ -15,7 +15,12 @@
*/ */
export default class UtilityScript { export default class UtilityScript {
evaluate(functionText: string, ...args: any[]) { evaluate(returnByValue: boolean, expression: string) {
const result = global.eval(expression);
return returnByValue ? this._serialize(result) : result;
}
callFunction(returnByValue: boolean, functionText: string, ...args: any[]) {
const argCount = args[0] as number; const argCount = args[0] as number;
const handleCount = args[argCount + 1] as number; const handleCount = args[argCount + 1] as number;
const handles = { __proto__: null } as any; const handles = { __proto__: null } as any;
@ -34,6 +39,19 @@ export default class UtilityScript {
for (let i = 0; i < argCount; i++) for (let i = 0; i < argCount; i++)
processedArgs[i] = visit(args[i + 1]); processedArgs[i] = visit(args[i + 1]);
const func = global.eval('(' + functionText + ')'); const func = global.eval('(' + functionText + ')');
return func(...processedArgs); const result = func(...processedArgs);
return returnByValue ? this._serialize(result) : result;
}
private _serialize(value: any): any {
if (value instanceof Error) {
const error = value;
if ('captureStackTrace' in global.Error) {
// v8
return error.stack;
}
return `${error.name}: ${error.message}\n${error.stack}`;
}
return value;
} }
} }

View file

@ -55,8 +55,8 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
async evaluate(context: js.ExecutionContext, returnByValue: boolean, pageFunction: Function | string, ...args: any[]): Promise<any> { async evaluate(context: js.ExecutionContext, returnByValue: boolean, pageFunction: Function | string, ...args: any[]): Promise<any> {
try { try {
let response = await this._evaluateRemoteObject(context, pageFunction, args); let response = await this._evaluateRemoteObject(context, pageFunction, args, returnByValue);
if (response.result.type === 'object' && 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),
this._session.send('Runtime.awaitPromise', { this._session.send('Runtime.awaitPromise', {
@ -79,14 +79,15 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
} }
} }
private async _evaluateRemoteObject(context: js.ExecutionContext, pageFunction: Function | string, args: any[]): Promise<any> { private async _evaluateRemoteObject(context: js.ExecutionContext, pageFunction: Function | string, args: any[], returnByValue: boolean): Promise<Protocol.Runtime.callFunctionOnReturnValue> {
if (helper.isString(pageFunction)) { if (helper.isString(pageFunction)) {
const contextId = this._contextId; const utilityScript = await context.utilityScript();
const expression: string = pageFunction; const functionDeclaration = `function (returnByValue, pageFunction) { return this.evaluate(returnByValue, pageFunction); }${js.generateSourceUrl()}`;
return await this._session.send('Runtime.evaluate', { return await this._session.send('Runtime.callFunctionOn', {
expression: js.ensureSourceUrl(expression), functionDeclaration,
contextId, objectId: utilityScript._remoteObject.objectId!,
returnByValue: false, arguments: [ { value: returnByValue }, { value: pageFunction } ],
returnByValue: false, // We need to return real Promise if that is a promise.
emulateUserGesture: true emulateUserGesture: true
}); });
} }
@ -110,12 +111,12 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
try { try {
const utilityScript = await context.utilityScript(); const utilityScript = await context.utilityScript();
const callParams = this._serializeFunctionAndArguments(functionText, values, handles); const callParams = this._serializeFunctionAndArguments(functionText, values, handles, returnByValue);
return await this._session.send('Runtime.callFunctionOn', { return await this._session.send('Runtime.callFunctionOn', {
functionDeclaration: callParams.functionText, functionDeclaration: callParams.functionText,
objectId: utilityScript._remoteObject.objectId!, objectId: utilityScript._remoteObject.objectId!,
arguments: [ ...callParams.callArguments ], arguments: callParams.callArguments,
returnByValue: false, returnByValue: false, // We need to return real Promise if that is a promise.
emulateUserGesture: true emulateUserGesture: true
}); });
} finally { } finally {
@ -123,9 +124,9 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
} }
} }
private _serializeFunctionAndArguments(originalText: string, values: any[], handles: MaybeCallArgument[]): { functionText: string, callArguments: Protocol.Runtime.CallArgument[] } { private _serializeFunctionAndArguments(originalText: string, values: any[], handles: MaybeCallArgument[], returnByValue: boolean): { functionText: string, callArguments: Protocol.Runtime.CallArgument[]} {
const callArguments: Protocol.Runtime.CallArgument[] = values.map(value => ({ value })); const callArguments: Protocol.Runtime.CallArgument[] = values.map(value => ({ value }));
let functionText = `function (functionText, ...args) { return this.evaluate(functionText, ...args); }${js.generateSourceUrl()}`; let functionText = `function (returnByValue, functionText, ...args) { return this.callFunction(returnByValue, functionText, ...args); }${js.generateSourceUrl()}`;
if (handles.some(handle => 'unserializable' in handle)) { if (handles.some(handle => 'unserializable' in handle)) {
const paramStrings = []; const paramStrings = [];
for (let i = 0; i < callArguments.length; i++) for (let i = 0; i < callArguments.length; i++)
@ -138,11 +139,11 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
callArguments.push(handle); callArguments.push(handle);
} }
} }
functionText = `function (functionText, ...a) { return this.evaluate(functionText, ${paramStrings.join(',')}); }${js.generateSourceUrl()}`; functionText = `function (returnByValue, functionText, ...a) { return this.callFunction(returnByValue, functionText, ${paramStrings.join(',')}); }${js.generateSourceUrl()}`;
} else { } else {
callArguments.push(...(handles as Protocol.Runtime.CallArgument[])); callArguments.push(...(handles as Protocol.Runtime.CallArgument[]));
} }
return { functionText, callArguments: [ { value: originalText }, ...callArguments ] }; return { functionText, callArguments: [ { value: returnByValue }, { value: originalText }, ...callArguments ] };
function unserializableToString(arg: any) { function unserializableToString(arg: any) {
if (Object.is(arg, -0)) if (Object.is(arg, -0))

View file

@ -42,13 +42,15 @@ describe('ChromiumBrowserContext.createSession', function() {
// JS coverage enables and then disables Debugger domain. // JS coverage enables and then disables Debugger domain.
await page.coverage.startJSCoverage(); await page.coverage.startJSCoverage();
await page.coverage.stopJSCoverage(); await page.coverage.stopJSCoverage();
page.on('console', console.log);
// generate a script in page and wait for the event. // generate a script in page and wait for the event.
const [event] = await Promise.all([ await Promise.all([
new Promise(f => client.on('Debugger.scriptParsed', f)), new Promise(f => client.on('Debugger.scriptParsed', event => {
if (event.url === 'foo.js')
f();
})),
page.evaluate('//# sourceURL=foo.js') page.evaluate('//# sourceURL=foo.js')
]); ]);
// expect events to be dispatched.
expect(event.url).toBe('foo.js');
}); });
it('should be able to detach session', async function({page, browser, server}) { it('should be able to detach session', async function({page, browser, server}) {
const client = await page.context().newCDPSession(page); const client = await page.context().newCDPSession(page);

View file

@ -326,6 +326,35 @@ describe('Page.evaluate', function() {
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
expect(await page.evaluate(() => 2 + 2)).toBe(4); expect(await page.evaluate(() => 2 + 2)).toBe(4);
}); });
it('should evaluate exception', async({page, server}) => {
const error = await page.evaluate(() => {
return (function functionOnStack() {
return new Error('error message');
})();
});
expect(error).toContain('Error: error message');
expect(error).toContain('functionOnStack');
});
it('should evaluate exception', async({page, server}) => {
const error = await page.evaluate(`new Error('error message')`);
expect(error).toContain('Error: error message');
});
it('should evaluate date as {}', async({page}) => {
const result = await page.evaluate(() => ({ date: new Date() }));
expect(result).toEqual({ date: {} });
});
it('should jsonValue() date as {}', async({page}) => {
const resultHandle = await page.evaluateHandle(() => ({ date: new Date() }));
expect(await resultHandle.jsonValue()).toEqual({ date: {} });
});
it.fail(FFOX)('should not use toJSON when evaluating', async({page, server}) => {
const result = await page.evaluate(() => ({ toJSON: () => 'string', data: 'data' }));
expect(result).toEqual({ data: 'data', toJSON: {} });
});
it.fail(FFOX)('should not use toJSON in jsonValue', async({page, server}) => {
const resultHandle = await page.evaluateHandle(() => ({ toJSON: () => 'string', data: 'data' }));
expect(await resultHandle.jsonValue()).toEqual({ data: 'data', toJSON: {} });
});
}); });
describe('Page.addInitScript', function() { describe('Page.addInitScript', function() {