feat(evaluate): return user-readable error from evaluate (#2329)
This commit is contained in:
parent
0a8fa6e46c
commit
5ee6494032
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue