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:
Dmitry Gozman 2020-06-03 17:50:16 -07:00 committed by GitHub
parent 1392dcd680
commit d5c992e1db
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 124 additions and 208 deletions

View file

@ -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 {

View file

@ -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);
}); });
} }

View file

@ -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) {

View file

@ -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 {

View file

@ -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);
} }
} }

View file

@ -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();

View file

@ -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;
}

View file

@ -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);