Skip to content

Commit aa0d844

Browse files
authored
chore: introduce utility script for evaluate helpers (#2306)
1 parent d99ebc9 commit aa0d844

13 files changed

+242
-117
lines changed

src/chromium/crExecutionContext.ts

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,16 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
3030
this._contextId = contextPayload.id;
3131
}
3232

33+
async rawEvaluate(expression: string): Promise<js.RemoteObject> {
34+
const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.evaluate', {
35+
expression: js.ensureSourceUrl(expression),
36+
contextId: this._contextId,
37+
}).catch(rewriteError);
38+
if (exceptionDetails)
39+
throw new Error('Evaluation failed: ' + getExceptionMessage(exceptionDetails));
40+
return remoteObject;
41+
}
42+
3343
async evaluate(context: js.ExecutionContext, returnByValue: boolean, pageFunction: Function | string, ...args: any[]): Promise<any> {
3444
if (helper.isString(pageFunction)) {
3545
const contextId = this._contextId;
@@ -72,10 +82,12 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
7282
});
7383

7484
try {
85+
const utilityScript = await context.utilityScript();
7586
const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.callFunctionOn', {
76-
functionDeclaration: functionText,
77-
executionContextId: this._contextId,
87+
functionDeclaration: `function (...args) { return this.evaluate(...args) }${js.generateSourceUrl()}`,
88+
objectId: utilityScript._remoteObject.objectId,
7889
arguments: [
90+
{ value: functionText },
7991
...values.map(value => ({ value })),
8092
...handles,
8193
],
@@ -89,19 +101,6 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
89101
} finally {
90102
dispose();
91103
}
92-
93-
function rewriteError(error: Error): Protocol.Runtime.evaluateReturnValue {
94-
if (error.message.includes('Object reference chain is too long'))
95-
return {result: {type: 'undefined'}};
96-
if (error.message.includes('Object couldn\'t be returned by value'))
97-
return {result: {type: 'undefined'}};
98-
99-
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.'))
100-
throw new Error('Execution context was destroyed, most likely because of a navigation.');
101-
if (error instanceof TypeError && error.message.startsWith('Converting circular structure to JSON'))
102-
error.message += ' Are you passing a nested JSHandle?';
103-
throw error;
104-
}
105104
}
106105

107106
async getProperties(handle: js.JSHandle): Promise<Map<string, js.JSHandle>> {
@@ -152,3 +151,16 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
152151
function toRemoteObject(handle: js.JSHandle): Protocol.Runtime.RemoteObject {
153152
return handle._remoteObject as Protocol.Runtime.RemoteObject;
154153
}
154+
155+
function rewriteError(error: Error): Protocol.Runtime.evaluateReturnValue {
156+
if (error.message.includes('Object reference chain is too long'))
157+
return {result: {type: 'undefined'}};
158+
if (error.message.includes('Object couldn\'t be returned by value'))
159+
return {result: {type: 'undefined'}};
160+
161+
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.'))
162+
throw new Error('Execution context was destroyed, most likely because of a navigation.');
163+
if (error instanceof TypeError && error.message.startsWith('Converting circular structure to JSON'))
164+
error.message += ' Are you passing a nested JSHandle?';
165+
throw error;
166+
}

src/chromium/crPage.ts

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -760,7 +760,7 @@ class FrameSession {
760760

761761
async _getContentFrame(handle: dom.ElementHandle): Promise<frames.Frame | null> {
762762
const nodeInfo = await this._client.send('DOM.describeNode', {
763-
objectId: toRemoteObject(handle).objectId
763+
objectId: handle._remoteObject.objectId
764764
});
765765
if (!nodeInfo || typeof nodeInfo.node.frameId !== 'string')
766766
return null;
@@ -777,7 +777,7 @@ class FrameSession {
777777
});
778778
if (!documentElement)
779779
return null;
780-
const remoteObject = toRemoteObject(documentElement);
780+
const remoteObject = documentElement._remoteObject;
781781
if (!remoteObject.objectId)
782782
return null;
783783
const nodeInfo = await this._client.send('DOM.describeNode', {
@@ -791,7 +791,7 @@ class FrameSession {
791791

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

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

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

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

854-
function toRemoteObject(handle: js.JSHandle): Protocol.Runtime.RemoteObject {
855-
return handle._remoteObject as Protocol.Runtime.RemoteObject;
856-
}
857-
858854
async function emulateLocale(session: CRSession, locale: string) {
859855
try {
860856
await session.send('Emulation.setLocaleOverride', { locale });

src/dom.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ export class FrameExecutionContext extends js.ExecutionContext {
8181
${custom.join(',\n')}
8282
])
8383
`;
84-
this._injectedPromise = this.doEvaluateInternal(false /* returnByValue */, false /* waitForNavigations */, source);
84+
this._injectedPromise = this._delegate.rawEvaluate(source).then(object => this.createHandle(object));
8585
}
8686
return this._injectedPromise;
8787
}

src/firefox/ffExecutionContext.ts

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
2929
this._executionContextId = executionContextId;
3030
}
3131

32+
async rawEvaluate(expression: string): Promise<js.RemoteObject> {
33+
const payload = await this._session.send('Runtime.evaluate', {
34+
expression: js.ensureSourceUrl(expression),
35+
returnByValue: false,
36+
executionContextId: this._executionContextId,
37+
}).catch(rewriteError);
38+
checkException(payload.exceptionDetails);
39+
return payload.result!;
40+
}
41+
3242
async evaluate(context: js.ExecutionContext, returnByValue: boolean, pageFunction: Function | string, ...args: any[]): Promise<any> {
3343
if (helper.isString(pageFunction)) {
3444
const payload = await this._session.send('Runtime.evaluate', {
@@ -59,9 +69,12 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
5969
});
6070

6171
try {
72+
const utilityScript = await context.utilityScript();
6273
const payload = await this._session.send('Runtime.callFunction', {
63-
functionDeclaration: functionText,
74+
functionDeclaration: `(utilityScript, ...args) => utilityScript.evaluate(...args)`,
6475
args: [
76+
{ objectId: utilityScript._remoteObject.objectId, value: undefined },
77+
{ value: functionText },
6578
...values.map(value => ({ value })),
6679
...handles,
6780
],
@@ -75,16 +88,6 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
7588
} finally {
7689
dispose();
7790
}
78-
79-
function rewriteError(error: Error): (Protocol.Runtime.evaluateReturnValue | Protocol.Runtime.callFunctionReturnValue) {
80-
if (error.message.includes('cyclic object value') || error.message.includes('Object is not serializable'))
81-
return {result: {type: 'undefined', value: undefined}};
82-
if (error.message.includes('Failed to find execution context with id') || error.message.includes('Execution context was destroyed!'))
83-
throw new Error('Execution context was destroyed, most likely because of a navigation.');
84-
if (error instanceof TypeError && error.message.startsWith('Converting circular structure to JSON'))
85-
error.message += ' Are you passing a nested JSHandle?';
86-
throw error;
87-
}
8891
}
8992

9093
async getProperties(handle: js.JSHandle): Promise<Map<string, js.JSHandle>> {
@@ -113,7 +116,7 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
113116
async handleJSONValue<T>(handle: js.JSHandle<T>): Promise<T> {
114117
const payload = handle._remoteObject;
115118
if (!payload.objectId)
116-
return deserializeValue(payload);
119+
return deserializeValue(payload as Protocol.Runtime.RemoteObject);
117120
const simpleValue = await this._session.send('Runtime.callFunction', {
118121
executionContextId: this._executionContextId,
119122
returnByValue: true,
@@ -127,7 +130,7 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
127130
const payload = handle._remoteObject;
128131
if (payload.objectId)
129132
return 'JSHandle@' + (payload.subtype || payload.type);
130-
return (includeType ? 'JSHandle:' : '') + deserializeValue(payload);
133+
return (includeType ? 'JSHandle:' : '') + deserializeValue(payload as Protocol.Runtime.RemoteObject);
131134
}
132135

133136
private _toCallArgument(payload: any): any {
@@ -155,3 +158,13 @@ export function deserializeValue({unserializableValue, value}: Protocol.Runtime.
155158
return NaN;
156159
return value;
157160
}
161+
162+
function rewriteError(error: Error): (Protocol.Runtime.evaluateReturnValue | Protocol.Runtime.callFunctionReturnValue) {
163+
if (error.message.includes('cyclic object value') || error.message.includes('Object is not serializable'))
164+
return {result: {type: 'undefined', value: undefined}};
165+
if (error.message.includes('Failed to find execution context with id') || error.message.includes('Execution context was destroyed!'))
166+
throw new Error('Execution context was destroyed, most likely because of a navigation.');
167+
if (error instanceof TypeError && error.message.startsWith('Converting circular structure to JSON'))
168+
error.message += ' Are you passing a nested JSHandle?';
169+
throw error;
170+
}

src/firefox/ffPage.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,7 @@ export class FFPage implements PageDelegate {
373373
async getContentFrame(handle: dom.ElementHandle): Promise<frames.Frame | null> {
374374
const { contentFrameId } = await this._session.send('Page.describeNode', {
375375
frameId: handle._context.frame._id,
376-
objectId: toRemoteObject(handle).objectId!,
376+
objectId: handle._remoteObject.objectId!,
377377
});
378378
if (!contentFrameId)
379379
return null;
@@ -383,7 +383,7 @@ export class FFPage implements PageDelegate {
383383
async getOwnerFrame(handle: dom.ElementHandle): Promise<string | null> {
384384
const { ownerFrameId } = await this._session.send('Page.describeNode', {
385385
frameId: handle._context.frame._id,
386-
objectId: toRemoteObject(handle).objectId!,
386+
objectId: handle._remoteObject.objectId!,
387387
});
388388
return ownerFrameId || null;
389389
}
@@ -414,7 +414,7 @@ export class FFPage implements PageDelegate {
414414
async scrollRectIntoViewIfNeeded(handle: dom.ElementHandle, rect?: types.Rect): Promise<void> {
415415
await this._session.send('Page.scrollIntoViewIfNeeded', {
416416
frameId: handle._context.frame._id,
417-
objectId: toRemoteObject(handle).objectId!,
417+
objectId: handle._remoteObject.objectId!,
418418
rect,
419419
}).catch(e => {
420420
if (e instanceof Error && e.message.includes('Node is detached from document'))
@@ -433,7 +433,7 @@ export class FFPage implements PageDelegate {
433433
async getContentQuads(handle: dom.ElementHandle): Promise<types.Quad[] | null> {
434434
const result = await this._session.send('Page.getContentQuads', {
435435
frameId: handle._context.frame._id,
436-
objectId: toRemoteObject(handle).objectId!,
436+
objectId: handle._remoteObject.objectId!,
437437
}).catch(logError(this._page));
438438
if (!result)
439439
return null;
@@ -452,7 +452,7 @@ export class FFPage implements PageDelegate {
452452
async adoptElementHandle<T extends Node>(handle: dom.ElementHandle<T>, to: dom.FrameExecutionContext): Promise<dom.ElementHandle<T>> {
453453
const result = await this._session.send('Page.adoptNode', {
454454
frameId: handle._context.frame._id,
455-
objectId: toRemoteObject(handle).objectId!,
455+
objectId: handle._remoteObject.objectId!,
456456
executionContextId: (to._delegate as FFExecutionContext)._executionContextId
457457
});
458458
if (!result.remoteObject)
@@ -483,7 +483,3 @@ export class FFPage implements PageDelegate {
483483
return result.handle;
484484
}
485485
}
486-
487-
function toRemoteObject(handle: dom.ElementHandle): Protocol.Runtime.RemoteObject {
488-
return handle._remoteObject;
489-
}

src/injected/utilityScript.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* Copyright (c) Microsoft Corporation.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
export default class UtilityScript {
18+
evaluate(functionText: string, ...args: any[]) {
19+
const argCount = args[0] as number;
20+
const handleCount = args[argCount + 1] as number;
21+
const handles = { __proto__: null } as any;
22+
for (let i = 0; i < handleCount; i++)
23+
handles[args[argCount + 2 + i]] = args[argCount + 2 + handleCount + i];
24+
const visit = (arg: any) => {
25+
if ((typeof arg === 'string') && (arg in handles))
26+
return handles[arg];
27+
if (arg && (typeof arg === 'object')) {
28+
for (const name of Object.keys(arg))
29+
arg[name] = visit(arg[name]);
30+
}
31+
return arg;
32+
};
33+
const processedArgs = [];
34+
for (let i = 0; i < argCount; i++)
35+
processedArgs[i] = visit(args[i + 1]);
36+
const func = global.eval('(' + functionText + ')');
37+
return func(...processedArgs);
38+
}
39+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/**
2+
* Copyright (c) Microsoft Corporation.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
const path = require('path');
18+
const InlineSource = require('./webpack-inline-source-plugin.js');
19+
20+
module.exports = {
21+
entry: path.join(__dirname, 'utilityScript.ts'),
22+
devtool: 'source-map',
23+
module: {
24+
rules: [
25+
{
26+
test: /\.tsx?$/,
27+
loader: 'ts-loader',
28+
options: {
29+
transpileOnly: true
30+
},
31+
exclude: /node_modules/
32+
}
33+
]
34+
},
35+
resolve: {
36+
extensions: [ '.tsx', '.ts', '.js' ]
37+
},
38+
output: {
39+
libraryTarget: 'var',
40+
filename: 'utilityScriptSource.js',
41+
path: path.resolve(__dirname, '../../lib/injected/packed')
42+
},
43+
plugins: [
44+
new InlineSource(path.join(__dirname, '..', 'generated', 'utilityScriptSource.ts')),
45+
]
46+
};

0 commit comments

Comments
 (0)