chore(webkit): use async/await to make eval more readable (#789)

This commit is contained in:
Yury Semikhatsky 2020-01-31 17:23:17 -08:00 committed by GitHub
parent 0f305e05e9
commit b8199c0813
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -25,15 +25,14 @@ export const EVALUATION_SCRIPT_URL = '__playwright_evaluation_script__';
const SOURCE_URL_REGEX = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m; const SOURCE_URL_REGEX = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m;
export class WKExecutionContext implements js.ExecutionContextDelegate { export class WKExecutionContext implements js.ExecutionContextDelegate {
private _globalObjectId?: Promise<string>; private _globalObjectIdPromise?: Promise<Protocol.Runtime.RemoteObjectId>;
_session: WKSession; private readonly _session: WKSession;
_contextId: number | undefined; readonly _contextId: number | undefined;
private _contextDestroyedCallback: () => void = () => {}; private _contextDestroyedCallback: () => void = () => {};
private _executionContextDestroyedPromise: Promise<unknown>; private readonly _executionContextDestroyedPromise: Promise<unknown>;
_jsonStringifyObjectId: Protocol.Runtime.RemoteObjectId | undefined;
constructor(client: WKSession, contextId: number | undefined) { constructor(session: WKSession, contextId: number | undefined) {
this._session = client; this._session = session;
this._contextId = contextId; this._contextId = contextId;
this._executionContextDestroyedPromise = new Promise((resolve, reject) => { this._executionContextDestroyedPromise = new Promise((resolve, reject) => {
this._contextDestroyedCallback = resolve; this._contextDestroyedCallback = resolve;
@ -45,37 +44,65 @@ 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> {
if (helper.isString(pageFunction)) { try {
const contextId = this._contextId; let response = await this._evaluateRemoteObject(pageFunction, args);
const expression: string = pageFunction as string;
const expressionWithSourceUrl = SOURCE_URL_REGEX.test(expression) ? expression : expression + '\n' + suffix;
return this._session.send('Runtime.evaluate', {
expression: expressionWithSourceUrl,
contextId,
returnByValue: false,
emulateUserGesture: true
}).then(response => {
if (response.result.type === 'object' && response.result.className === 'Promise') { if (response.result.type === 'object' && response.result.className === 'Promise') {
return Promise.race([ response = await Promise.race([
this._executionContextDestroyedPromise.then(() => contextDestroyedResult), this._executionContextDestroyedPromise.then(() => contextDestroyedResult),
this._awaitPromise(response.result.objectId!), this._session.send('Runtime.awaitPromise', {
promiseObjectId: response.result.objectId,
returnByValue: false
})
]); ]);
} }
return response;
}).then(response => {
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 context._createHandle(response.result);
if (response.result.objectId) if (response.result.objectId)
return this._returnObjectByValue(response.result.objectId); return await this._returnObjectByValue(response.result.objectId);
return valueFromRemoteObject(response.result); return valueFromRemoteObject(response.result);
}).catch(rewriteError); } catch (error) {
if (isSwappedOutError(error) || error.message.includes('Missing injected script for given'))
throw new Error('Execution context was destroyed, most likely because of a navigation.');
throw error;
}
}
private async _evaluateRemoteObject(pageFunction: Function | string, args: any[]): Promise<any> {
if (helper.isString(pageFunction)) {
const contextId = this._contextId;
const expression: string = pageFunction as string;
const expressionWithSourceUrl = SOURCE_URL_REGEX.test(expression) ? expression : expression + '\n' + suffix;
return await this._session.send('Runtime.evaluate', {
expression: expressionWithSourceUrl,
contextId,
returnByValue: false,
emulateUserGesture: true
});
} }
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.`);
try {
const callParams = this._serializeFunctionAndArguments(pageFunction, args);
const thisObjectId = await this._contextGlobalObjectId();
return await this._session.send('Runtime.callFunctionOn', {
functionDeclaration: callParams.functionText + '\n' + suffix + '\n',
objectId: thisObjectId,
arguments: callParams.callArguments,
returnByValue: false,
emulateUserGesture: true
});
} catch (err) {
if (err instanceof TypeError && err.message.startsWith('Converting circular structure to JSON'))
err.message += ' Are you passing a nested JSHandle?';
throw err;
}
}
private _serializeFunctionAndArguments(pageFunction: Function, args: any[]): { functionText: string, callArguments: Protocol.Runtime.CallArgument[] } {
let functionText = pageFunction.toString(); let functionText = pageFunction.toString();
try { try {
new Function('(' + functionText + ')'); new Function('(' + functionText + ')');
@ -110,43 +137,8 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
} else { } else {
serializableArgs = args; serializableArgs = args;
} }
const serialized = serializableArgs.map((arg: any) => this._convertArgument(arg));
let thisObjectId; return { functionText, callArguments: serialized };
try {
thisObjectId = await this._contextGlobalObjectId();
} catch (error) {
if (error.message.includes('Missing injected script for given'))
throw new Error('Execution context was destroyed, most likely because of a navigation.');
throw error;
}
return this._session.send('Runtime.callFunctionOn', {
functionDeclaration: functionText + '\n' + suffix + '\n',
objectId: thisObjectId,
arguments: serializableArgs.map((arg: any) => this._convertArgument(arg)),
returnByValue: false,
emulateUserGesture: true
}).catch(err => {
if (err instanceof TypeError && err.message.startsWith('Converting circular structure to JSON'))
err.message += ' Are you passing a nested JSHandle?';
throw err;
}).then(response => {
if (response.result.type === 'object' && response.result.className === 'Promise') {
return Promise.race([
this._executionContextDestroyedPromise.then(() => contextDestroyedResult),
this._awaitPromise(response.result.objectId!),
]);
}
return response;
}).then(response => {
if (response.wasThrown)
throw new Error('Evaluation failed: ' + response.result.description);
if (!returnByValue)
return context._createHandle(response.result);
if (response.result.objectId)
return this._returnObjectByValue(response.result.objectId).catch(() => undefined);
return valueFromRemoteObject(response.result);
}).catch(rewriteError);
function unserializableToString(arg: any) { function unserializableToString(arg: any) {
if (Object.is(arg, -0)) if (Object.is(arg, -0))
@ -183,56 +175,36 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
} }
return false; return false;
} }
function rewriteError(error: Error): Protocol.Runtime.evaluateReturnValue {
if (error.message.includes('Missing injected script for given'))
throw new Error('Execution context was destroyed, most likely because of a navigation.');
throw error;
}
} }
private _contextGlobalObjectId() { private _contextGlobalObjectId() : Promise<Protocol.Runtime.RemoteObjectId> {
if (!this._globalObjectId) { if (!this._globalObjectIdPromise) {
this._globalObjectId = this._session.send('Runtime.evaluate', { this._globalObjectIdPromise = this._session.send('Runtime.evaluate', {
expression: 'this', expression: 'this',
contextId: this._contextId contextId: this._contextId
}).catch(e => {
if (isSwappedOutError(e))
throw new Error('Execution context was destroyed, most likely because of a navigation.');
throw e;
}).then(response => { }).then(response => {
return response.result.objectId!; return response.result.objectId!;
}); });
} }
return this._globalObjectId; return this._globalObjectIdPromise;
} }
private _awaitPromise(objectId: Protocol.Runtime.RemoteObjectId) { private async _returnObjectByValue(objectId: Protocol.Runtime.RemoteObjectId) : Promise<any> {
return this._session.send('Runtime.awaitPromise', { try {
promiseObjectId: objectId, const serializeResponse = await this._session.send('Runtime.callFunctionOn', {
returnByValue: false
}).catch(e => {
if (isSwappedOutError(e))
return contextDestroyedResult;
throw e;
});
}
private _returnObjectByValue(objectId: Protocol.Runtime.RemoteObjectId) {
return this._session.send('Runtime.callFunctionOn', {
// Serialize object using standard JSON implementation to correctly pass 'undefined'. // Serialize object using standard JSON implementation to correctly pass 'undefined'.
functionDeclaration: 'function(){return this}\n' + suffix + '\n', functionDeclaration: 'function(){return this}\n' + suffix + '\n',
objectId: objectId, objectId: objectId,
returnByValue: true returnByValue: true
}).catch(e => { });
if (serializeResponse.wasThrown)
return undefined;
return serializeResponse.result.value;
} catch (e) {
if (isSwappedOutError(e)) if (isSwappedOutError(e))
return contextDestroyedResult; return contextDestroyedResult;
throw e; return undefined;
}).then(serializeResponse => { }
if (serializeResponse.wasThrown)
throw new Error('Serialization failed: ' + serializeResponse.result.description);
return serializeResponse.result.value;
});
} }
async getProperties(handle: js.JSHandle): Promise<Map<string, js.JSHandle>> { async getProperties(handle: js.JSHandle): Promise<Map<string, js.JSHandle>> {