Skip to content

Commit

Permalink
chore: introduce utility script for evaluate helpers (#2306)
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelfeldman committed May 20, 2020
1 parent d99ebc9 commit aa0d844
Show file tree
Hide file tree
Showing 13 changed files with 242 additions and 117 deletions.
42 changes: 27 additions & 15 deletions src/chromium/crExecutionContext.ts
Expand Up @@ -30,6 +30,16 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
this._contextId = contextPayload.id;
}

async rawEvaluate(expression: string): Promise<js.RemoteObject> {
const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.evaluate', {
expression: js.ensureSourceUrl(expression),
contextId: this._contextId,
}).catch(rewriteError);
if (exceptionDetails)
throw new Error('Evaluation failed: ' + getExceptionMessage(exceptionDetails));
return remoteObject;
}

async evaluate(context: js.ExecutionContext, returnByValue: boolean, pageFunction: Function | string, ...args: any[]): Promise<any> {
if (helper.isString(pageFunction)) {
const contextId = this._contextId;
Expand Down Expand Up @@ -72,10 +82,12 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
});

try {
const utilityScript = await context.utilityScript();
const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.callFunctionOn', {
functionDeclaration: functionText,
executionContextId: this._contextId,
functionDeclaration: `function (...args) { return this.evaluate(...args) }${js.generateSourceUrl()}`,
objectId: utilityScript._remoteObject.objectId,
arguments: [
{ value: functionText },
...values.map(value => ({ value })),
...handles,
],
Expand All @@ -89,19 +101,6 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
} finally {
dispose();
}

function rewriteError(error: Error): Protocol.Runtime.evaluateReturnValue {
if (error.message.includes('Object reference chain is too long'))
return {result: {type: 'undefined'}};
if (error.message.includes('Object couldn\'t be returned by value'))
return {result: {type: 'undefined'}};

if (error.message.endsWith('Cannot find context with specified id') || error.message.endsWith('Inspected target navigated or closed') || error.message.endsWith('Execution context was destroyed.'))
throw new Error('Execution context was destroyed, most likely because of a navigation.');
if (error instanceof TypeError && error.message.startsWith('Converting circular structure to JSON'))
error.message += ' Are you passing a nested JSHandle?';
throw error;
}
}

async getProperties(handle: js.JSHandle): Promise<Map<string, js.JSHandle>> {
Expand Down Expand Up @@ -152,3 +151,16 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
function toRemoteObject(handle: js.JSHandle): Protocol.Runtime.RemoteObject {
return handle._remoteObject as Protocol.Runtime.RemoteObject;
}

function rewriteError(error: Error): Protocol.Runtime.evaluateReturnValue {
if (error.message.includes('Object reference chain is too long'))
return {result: {type: 'undefined'}};
if (error.message.includes('Object couldn\'t be returned by value'))
return {result: {type: 'undefined'}};

if (error.message.endsWith('Cannot find context with specified id') || error.message.endsWith('Inspected target navigated or closed') || error.message.endsWith('Execution context was destroyed.'))
throw new Error('Execution context was destroyed, most likely because of a navigation.');
if (error instanceof TypeError && error.message.startsWith('Converting circular structure to JSON'))
error.message += ' Are you passing a nested JSHandle?';
throw error;
}
16 changes: 6 additions & 10 deletions src/chromium/crPage.ts
Expand Up @@ -760,7 +760,7 @@ class FrameSession {

async _getContentFrame(handle: dom.ElementHandle): Promise<frames.Frame | null> {
const nodeInfo = await this._client.send('DOM.describeNode', {
objectId: toRemoteObject(handle).objectId
objectId: handle._remoteObject.objectId
});
if (!nodeInfo || typeof nodeInfo.node.frameId !== 'string')
return null;
Expand All @@ -777,7 +777,7 @@ class FrameSession {
});
if (!documentElement)
return null;
const remoteObject = toRemoteObject(documentElement);
const remoteObject = documentElement._remoteObject;
if (!remoteObject.objectId)
return null;
const nodeInfo = await this._client.send('DOM.describeNode', {
Expand All @@ -791,7 +791,7 @@ class FrameSession {

async _getBoundingBox(handle: dom.ElementHandle): Promise<types.Rect | null> {
const result = await this._client.send('DOM.getBoxModel', {
objectId: toRemoteObject(handle).objectId
objectId: handle._remoteObject.objectId
}).catch(logError(this._page));
if (!result)
return null;
Expand All @@ -805,7 +805,7 @@ class FrameSession {

async _scrollRectIntoViewIfNeeded(handle: dom.ElementHandle, rect?: types.Rect): Promise<void> {
await this._client.send('DOM.scrollIntoViewIfNeeded', {
objectId: toRemoteObject(handle).objectId,
objectId: handle._remoteObject.objectId,
rect,
}).catch(e => {
if (e instanceof Error && e.message.includes('Node is detached from document'))
Expand All @@ -821,7 +821,7 @@ class FrameSession {

async _getContentQuads(handle: dom.ElementHandle): Promise<types.Quad[] | null> {
const result = await this._client.send('DOM.getContentQuads', {
objectId: toRemoteObject(handle).objectId
objectId: handle._remoteObject.objectId
}).catch(logError(this._page));
if (!result)
return null;
Expand All @@ -835,7 +835,7 @@ class FrameSession {

async _adoptElementHandle<T extends Node>(handle: dom.ElementHandle<T>, to: dom.FrameExecutionContext): Promise<dom.ElementHandle<T>> {
const nodeInfo = await this._client.send('DOM.describeNode', {
objectId: toRemoteObject(handle).objectId,
objectId: handle._remoteObject.objectId,
});
return this._adoptBackendNodeId(nodeInfo.node.backendNodeId, to) as Promise<dom.ElementHandle<T>>;
}
Expand All @@ -851,10 +851,6 @@ class FrameSession {
}
}

function toRemoteObject(handle: js.JSHandle): Protocol.Runtime.RemoteObject {
return handle._remoteObject as Protocol.Runtime.RemoteObject;
}

async function emulateLocale(session: CRSession, locale: string) {
try {
await session.send('Emulation.setLocaleOverride', { locale });
Expand Down
2 changes: 1 addition & 1 deletion src/dom.ts
Expand Up @@ -81,7 +81,7 @@ export class FrameExecutionContext extends js.ExecutionContext {
${custom.join(',\n')}
])
`;
this._injectedPromise = this.doEvaluateInternal(false /* returnByValue */, false /* waitForNavigations */, source);
this._injectedPromise = this._delegate.rawEvaluate(source).then(object => this.createHandle(object));
}
return this._injectedPromise;
}
Expand Down
39 changes: 26 additions & 13 deletions src/firefox/ffExecutionContext.ts
Expand Up @@ -29,6 +29,16 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
this._executionContextId = executionContextId;
}

async rawEvaluate(expression: string): Promise<js.RemoteObject> {
const payload = await this._session.send('Runtime.evaluate', {
expression: js.ensureSourceUrl(expression),
returnByValue: false,
executionContextId: this._executionContextId,
}).catch(rewriteError);
checkException(payload.exceptionDetails);
return payload.result!;
}

async evaluate(context: js.ExecutionContext, returnByValue: boolean, pageFunction: Function | string, ...args: any[]): Promise<any> {
if (helper.isString(pageFunction)) {
const payload = await this._session.send('Runtime.evaluate', {
Expand Down Expand Up @@ -59,9 +69,12 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
});

try {
const utilityScript = await context.utilityScript();
const payload = await this._session.send('Runtime.callFunction', {
functionDeclaration: functionText,
functionDeclaration: `(utilityScript, ...args) => utilityScript.evaluate(...args)`,
args: [
{ objectId: utilityScript._remoteObject.objectId, value: undefined },
{ value: functionText },
...values.map(value => ({ value })),
...handles,
],
Expand All @@ -75,16 +88,6 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
} finally {
dispose();
}

function rewriteError(error: Error): (Protocol.Runtime.evaluateReturnValue | Protocol.Runtime.callFunctionReturnValue) {
if (error.message.includes('cyclic object value') || error.message.includes('Object is not serializable'))
return {result: {type: 'undefined', value: undefined}};
if (error.message.includes('Failed to find execution context with id') || error.message.includes('Execution context was destroyed!'))
throw new Error('Execution context was destroyed, most likely because of a navigation.');
if (error instanceof TypeError && error.message.startsWith('Converting circular structure to JSON'))
error.message += ' Are you passing a nested JSHandle?';
throw error;
}
}

async getProperties(handle: js.JSHandle): Promise<Map<string, js.JSHandle>> {
Expand Down Expand Up @@ -113,7 +116,7 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
async handleJSONValue<T>(handle: js.JSHandle<T>): Promise<T> {
const payload = handle._remoteObject;
if (!payload.objectId)
return deserializeValue(payload);
return deserializeValue(payload as Protocol.Runtime.RemoteObject);
const simpleValue = await this._session.send('Runtime.callFunction', {
executionContextId: this._executionContextId,
returnByValue: true,
Expand All @@ -127,7 +130,7 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
const payload = handle._remoteObject;
if (payload.objectId)
return 'JSHandle@' + (payload.subtype || payload.type);
return (includeType ? 'JSHandle:' : '') + deserializeValue(payload);
return (includeType ? 'JSHandle:' : '') + deserializeValue(payload as Protocol.Runtime.RemoteObject);
}

private _toCallArgument(payload: any): any {
Expand Down Expand Up @@ -155,3 +158,13 @@ export function deserializeValue({unserializableValue, value}: Protocol.Runtime.
return NaN;
return value;
}

function rewriteError(error: Error): (Protocol.Runtime.evaluateReturnValue | Protocol.Runtime.callFunctionReturnValue) {
if (error.message.includes('cyclic object value') || error.message.includes('Object is not serializable'))
return {result: {type: 'undefined', value: undefined}};
if (error.message.includes('Failed to find execution context with id') || error.message.includes('Execution context was destroyed!'))
throw new Error('Execution context was destroyed, most likely because of a navigation.');
if (error instanceof TypeError && error.message.startsWith('Converting circular structure to JSON'))
error.message += ' Are you passing a nested JSHandle?';
throw error;
}
14 changes: 5 additions & 9 deletions src/firefox/ffPage.ts
Expand Up @@ -373,7 +373,7 @@ export class FFPage implements PageDelegate {
async getContentFrame(handle: dom.ElementHandle): Promise<frames.Frame | null> {
const { contentFrameId } = await this._session.send('Page.describeNode', {
frameId: handle._context.frame._id,
objectId: toRemoteObject(handle).objectId!,
objectId: handle._remoteObject.objectId!,
});
if (!contentFrameId)
return null;
Expand All @@ -383,7 +383,7 @@ export class FFPage implements PageDelegate {
async getOwnerFrame(handle: dom.ElementHandle): Promise<string | null> {
const { ownerFrameId } = await this._session.send('Page.describeNode', {
frameId: handle._context.frame._id,
objectId: toRemoteObject(handle).objectId!,
objectId: handle._remoteObject.objectId!,
});
return ownerFrameId || null;
}
Expand Down Expand Up @@ -414,7 +414,7 @@ export class FFPage implements PageDelegate {
async scrollRectIntoViewIfNeeded(handle: dom.ElementHandle, rect?: types.Rect): Promise<void> {
await this._session.send('Page.scrollIntoViewIfNeeded', {
frameId: handle._context.frame._id,
objectId: toRemoteObject(handle).objectId!,
objectId: handle._remoteObject.objectId!,
rect,
}).catch(e => {
if (e instanceof Error && e.message.includes('Node is detached from document'))
Expand All @@ -433,7 +433,7 @@ export class FFPage implements PageDelegate {
async getContentQuads(handle: dom.ElementHandle): Promise<types.Quad[] | null> {
const result = await this._session.send('Page.getContentQuads', {
frameId: handle._context.frame._id,
objectId: toRemoteObject(handle).objectId!,
objectId: handle._remoteObject.objectId!,
}).catch(logError(this._page));
if (!result)
return null;
Expand All @@ -452,7 +452,7 @@ export class FFPage implements PageDelegate {
async adoptElementHandle<T extends Node>(handle: dom.ElementHandle<T>, to: dom.FrameExecutionContext): Promise<dom.ElementHandle<T>> {
const result = await this._session.send('Page.adoptNode', {
frameId: handle._context.frame._id,
objectId: toRemoteObject(handle).objectId!,
objectId: handle._remoteObject.objectId!,
executionContextId: (to._delegate as FFExecutionContext)._executionContextId
});
if (!result.remoteObject)
Expand Down Expand Up @@ -483,7 +483,3 @@ export class FFPage implements PageDelegate {
return result.handle;
}
}

function toRemoteObject(handle: dom.ElementHandle): Protocol.Runtime.RemoteObject {
return handle._remoteObject;
}
39 changes: 39 additions & 0 deletions src/injected/utilityScript.ts
@@ -0,0 +1,39 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

export default class UtilityScript {
evaluate(functionText: string, ...args: any[]) {
const argCount = args[0] as number;
const handleCount = args[argCount + 1] as number;
const handles = { __proto__: null } as any;
for (let i = 0; i < handleCount; i++)
handles[args[argCount + 2 + i]] = args[argCount + 2 + handleCount + i];
const visit = (arg: any) => {
if ((typeof arg === 'string') && (arg in handles))
return handles[arg];
if (arg && (typeof arg === 'object')) {
for (const name of Object.keys(arg))
arg[name] = visit(arg[name]);
}
return arg;
};
const processedArgs = [];
for (let i = 0; i < argCount; i++)
processedArgs[i] = visit(args[i + 1]);
const func = global.eval('(' + functionText + ')');
return func(...processedArgs);
}
}
46 changes: 46 additions & 0 deletions src/injected/utilityScript.webpack.config.js
@@ -0,0 +1,46 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

const path = require('path');
const InlineSource = require('./webpack-inline-source-plugin.js');

module.exports = {
entry: path.join(__dirname, 'utilityScript.ts'),
devtool: 'source-map',
module: {
rules: [
{
test: /\.tsx?$/,
loader: 'ts-loader',
options: {
transpileOnly: true
},
exclude: /node_modules/
}
]
},
resolve: {
extensions: [ '.tsx', '.ts', '.js' ]
},
output: {
libraryTarget: 'var',
filename: 'utilityScriptSource.js',
path: path.resolve(__dirname, '../../lib/injected/packed')
},
plugins: [
new InlineSource(path.join(__dirname, '..', 'generated', 'utilityScriptSource.ts')),
]
};

0 comments on commit aa0d844

Please sign in to comment.