mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore: introduce utility script for evaluate helpers (#2306)
This commit is contained in:
parent
d99ebc9265
commit
aa0d844c76
@ -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;
|
||||
@ -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,
|
||||
],
|
||||
@ -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>> {
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
@ -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', {
|
||||
@ -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;
|
||||
@ -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'))
|
||||
@ -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;
|
||||
@ -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>>;
|
||||
}
|
||||
@ -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 });
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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', {
|
||||
@ -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,
|
||||
],
|
||||
@ -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>> {
|
||||
@ -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,
|
||||
@ -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 {
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
@ -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'))
|
||||
@ -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;
|
||||
@ -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)
|
||||
@ -483,7 +483,3 @@ export class FFPage implements PageDelegate {
|
||||
return result.handle;
|
||||
}
|
||||
}
|
||||
|
||||
function toRemoteObject(handle: dom.ElementHandle): Protocol.Runtime.RemoteObject {
|
||||
return handle._remoteObject;
|
||||
}
|
||||
|
||||
39
src/injected/utilityScript.ts
Normal file
39
src/injected/utilityScript.ts
Normal file
@ -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
src/injected/utilityScript.webpack.config.js
Normal file
46
src/injected/utilityScript.webpack.config.js
Normal file
@ -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')),
|
||||
]
|
||||
};
|
||||
@ -18,11 +18,14 @@ import * as types from './types';
|
||||
import * as dom from './dom';
|
||||
import * as fs from 'fs';
|
||||
import * as util from 'util';
|
||||
import * as js from './javascript';
|
||||
import * as utilityScriptSource from './generated/utilityScriptSource';
|
||||
import { helper, getCallerFilePath, isDebugMode } from './helper';
|
||||
import { InnerLogger } from './logger';
|
||||
|
||||
export interface ExecutionContextDelegate {
|
||||
evaluate(context: ExecutionContext, returnByValue: boolean, pageFunction: string | Function, ...args: any[]): Promise<any>;
|
||||
rawEvaluate(pageFunction: string): Promise<RemoteObject>;
|
||||
getProperties(handle: JSHandle): Promise<Map<string, JSHandle>>;
|
||||
releaseHandle(handle: JSHandle): Promise<void>;
|
||||
handleToString(handle: JSHandle, includeType: boolean): string;
|
||||
@ -32,6 +35,7 @@ export interface ExecutionContextDelegate {
|
||||
export class ExecutionContext {
|
||||
readonly _delegate: ExecutionContextDelegate;
|
||||
readonly _logger: InnerLogger;
|
||||
private _utilityScriptPromise: Promise<js.JSHandle> | undefined;
|
||||
|
||||
constructor(delegate: ExecutionContextDelegate, logger: InnerLogger) {
|
||||
this._delegate = delegate;
|
||||
@ -58,17 +62,32 @@ export class ExecutionContext {
|
||||
return this.doEvaluateInternal(false /* returnByValue */, true /* waitForNavigations */, pageFunction, ...args);
|
||||
}
|
||||
|
||||
utilityScript(): Promise<js.JSHandle> {
|
||||
if (!this._utilityScriptPromise) {
|
||||
const source = `new (${utilityScriptSource.source})()`;
|
||||
this._utilityScriptPromise = this._delegate.rawEvaluate(source).then(object => this.createHandle(object));
|
||||
}
|
||||
return this._utilityScriptPromise;
|
||||
}
|
||||
|
||||
createHandle(remoteObject: any): JSHandle {
|
||||
return new JSHandle(this, remoteObject);
|
||||
}
|
||||
}
|
||||
|
||||
export type RemoteObject = {
|
||||
type?: string,
|
||||
subtype?: string,
|
||||
objectId?: string,
|
||||
value?: any
|
||||
};
|
||||
|
||||
export class JSHandle<T = any> {
|
||||
readonly _context: ExecutionContext;
|
||||
readonly _remoteObject: any;
|
||||
readonly _remoteObject: RemoteObject;
|
||||
_disposed = false;
|
||||
|
||||
constructor(context: ExecutionContext, remoteObject: any) {
|
||||
constructor(context: ExecutionContext, remoteObject: RemoteObject) {
|
||||
this._context = context;
|
||||
this._remoteObject = remoteObject;
|
||||
}
|
||||
@ -202,39 +221,6 @@ export async function prepareFunctionCall<T>(
|
||||
if (error)
|
||||
throw new Error(error);
|
||||
|
||||
if (!guids.length) {
|
||||
const sourceMapUrl = await generateSourceMapUrl(originalText, { line: 0, column: 0 });
|
||||
functionText += sourceMapUrl;
|
||||
return { functionText, values: args, handles: [], dispose: () => {} };
|
||||
}
|
||||
|
||||
const wrappedFunctionText = `(...__playwright__args__) => {
|
||||
return (${functionText})(...(() => {
|
||||
const args = __playwright__args__;
|
||||
__playwright__args__ = undefined;
|
||||
const argCount = args[0];
|
||||
const handleCount = args[argCount + 1];
|
||||
const handles = { __proto__: null };
|
||||
for (let i = 0; i < handleCount; i++)
|
||||
handles[args[argCount + 2 + i]] = args[argCount + 2 + handleCount + i];
|
||||
const visit = (arg) => {
|
||||
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 result = [];
|
||||
for (let i = 0; i < argCount; i++)
|
||||
result[i] = visit(args[i + 1]);
|
||||
return result;
|
||||
})());
|
||||
}`;
|
||||
const compiledPosition = findPosition(wrappedFunctionText, wrappedFunctionText.indexOf(functionText));
|
||||
functionText = wrappedFunctionText;
|
||||
|
||||
const resolved = await Promise.all(handles);
|
||||
const resultHandles: T[] = [];
|
||||
for (let i = 0; i < resolved.length; i++) {
|
||||
@ -251,7 +237,7 @@ export async function prepareFunctionCall<T>(
|
||||
toDispose.map(handlePromise => handlePromise.then(handle => handle.dispose()));
|
||||
};
|
||||
|
||||
const sourceMapUrl = await generateSourceMapUrl(originalText, compiledPosition);
|
||||
const sourceMapUrl = await generateSourceMapUrl(originalText);
|
||||
functionText += sourceMapUrl;
|
||||
return { functionText, values: [ args.length, ...args, guids.length, ...guids ], handles: resultHandles, dispose };
|
||||
}
|
||||
@ -276,7 +262,7 @@ type Position = {
|
||||
column: number;
|
||||
};
|
||||
|
||||
async function generateSourceMapUrl(functionText: string, compiledPosition: Position): Promise<string> {
|
||||
async function generateSourceMapUrl(functionText: string): Promise<string> {
|
||||
if (!isDebugMode())
|
||||
return generateSourceUrl();
|
||||
const filePath = getCallerFilePath();
|
||||
@ -289,7 +275,7 @@ async function generateSourceMapUrl(functionText: string, compiledPosition: Posi
|
||||
return generateSourceUrl();
|
||||
const sourcePosition = findPosition(source, index);
|
||||
const delta = findPosition(functionText, functionText.length);
|
||||
const sourceMap = generateSourceMap(filePath, sourcePosition, compiledPosition, delta);
|
||||
const sourceMap = generateSourceMap(filePath, sourcePosition, { line: 0, column: 0 }, delta);
|
||||
return `\n//# sourceMappingURL=data:application/json;base64,${Buffer.from(sourceMap).toString('base64')}\n`;
|
||||
} catch (e) {
|
||||
return generateSourceUrl();
|
||||
|
||||
@ -24,7 +24,6 @@ import * as js from '../javascript';
|
||||
type MaybeCallArgument = Protocol.Runtime.CallArgument | { unserializable: any };
|
||||
|
||||
export class WKExecutionContext implements js.ExecutionContextDelegate {
|
||||
private _globalObjectIdPromise?: Promise<Protocol.Runtime.RemoteObjectId>;
|
||||
private readonly _session: WKSession;
|
||||
readonly _contextId: number | undefined;
|
||||
private _contextDestroyedCallback: () => void = () => {};
|
||||
@ -42,6 +41,18 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
|
||||
this._contextDestroyedCallback();
|
||||
}
|
||||
|
||||
async rawEvaluate(expression: string): Promise<js.RemoteObject> {
|
||||
const contextId = this._contextId;
|
||||
const response = await this._session.send('Runtime.evaluate', {
|
||||
expression: js.ensureSourceUrl(expression),
|
||||
contextId,
|
||||
returnByValue: false
|
||||
});
|
||||
if (response.wasThrown)
|
||||
throw new Error('Evaluation failed: ' + response.result.description);
|
||||
return response.result;
|
||||
}
|
||||
|
||||
async evaluate(context: js.ExecutionContext, returnByValue: boolean, pageFunction: Function | string, ...args: any[]): Promise<any> {
|
||||
try {
|
||||
let response = await this._evaluateRemoteObject(context, pageFunction, args);
|
||||
@ -87,7 +98,7 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
|
||||
if (typeof value === 'bigint' || Object.is(value, -0) || Object.is(value, Infinity) || Object.is(value, -Infinity) || Object.is(value, NaN))
|
||||
return { handle: { unserializable: value } };
|
||||
if (value && (value instanceof js.JSHandle)) {
|
||||
const remoteObject = toRemoteObject(value);
|
||||
const remoteObject = value._remoteObject;
|
||||
if (!remoteObject.objectId && !Object.is(valueFromRemoteObject(remoteObject), remoteObject.value))
|
||||
return { handle: { unserializable: value } };
|
||||
if (!remoteObject.objectId)
|
||||
@ -98,12 +109,12 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
|
||||
});
|
||||
|
||||
try {
|
||||
const utilityScript = await context.utilityScript();
|
||||
const callParams = this._serializeFunctionAndArguments(functionText, values, handles);
|
||||
const thisObjectId = await this._contextGlobalObjectId();
|
||||
return await this._session.send('Runtime.callFunctionOn', {
|
||||
functionDeclaration: callParams.functionText,
|
||||
objectId: thisObjectId,
|
||||
arguments: callParams.callArguments,
|
||||
objectId: utilityScript._remoteObject.objectId!,
|
||||
arguments: [ ...callParams.callArguments ],
|
||||
returnByValue: false,
|
||||
emulateUserGesture: true
|
||||
});
|
||||
@ -112,8 +123,9 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
private _serializeFunctionAndArguments(functionText: string, values: any[], handles: MaybeCallArgument[]): { functionText: string, callArguments: Protocol.Runtime.CallArgument[] } {
|
||||
private _serializeFunctionAndArguments(originalText: string, values: any[], handles: MaybeCallArgument[]): { functionText: string, callArguments: Protocol.Runtime.CallArgument[] } {
|
||||
const callArguments: Protocol.Runtime.CallArgument[] = values.map(value => ({ value }));
|
||||
let functionText = `function (functionText, ...args) { return this.evaluate(functionText, ...args); }${js.generateSourceUrl()}`;
|
||||
if (handles.some(handle => 'unserializable' in handle)) {
|
||||
const paramStrings = [];
|
||||
for (let i = 0; i < callArguments.length; i++)
|
||||
@ -126,11 +138,11 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
|
||||
callArguments.push(handle);
|
||||
}
|
||||
}
|
||||
functionText = `(...a) => (${functionText})(${paramStrings.join(',')})`;
|
||||
functionText = `function (functionText, ...a) { return this.evaluate(functionText, ${paramStrings.join(',')}); }${js.generateSourceUrl()}`;
|
||||
} else {
|
||||
callArguments.push(...(handles as Protocol.Runtime.CallArgument[]));
|
||||
}
|
||||
return { functionText, callArguments };
|
||||
return { functionText, callArguments: [ { value: originalText }, ...callArguments ] };
|
||||
|
||||
function unserializableToString(arg: any) {
|
||||
if (Object.is(arg, -0))
|
||||
@ -142,7 +154,7 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
|
||||
if (Object.is(arg, NaN))
|
||||
return 'NaN';
|
||||
if (arg instanceof js.JSHandle) {
|
||||
const remoteObj = toRemoteObject(arg);
|
||||
const remoteObj = arg._remoteObject;
|
||||
if (!remoteObj.objectId)
|
||||
return valueFromRemoteObject(remoteObj);
|
||||
}
|
||||
@ -150,18 +162,6 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
private _contextGlobalObjectId(): Promise<Protocol.Runtime.RemoteObjectId> {
|
||||
if (!this._globalObjectIdPromise) {
|
||||
this._globalObjectIdPromise = this._session.send('Runtime.evaluate', {
|
||||
expression: 'this',
|
||||
contextId: this._contextId
|
||||
}).then(response => {
|
||||
return response.result.objectId!;
|
||||
});
|
||||
}
|
||||
return this._globalObjectIdPromise;
|
||||
}
|
||||
|
||||
private async _returnObjectByValue(objectId: Protocol.Runtime.RemoteObjectId): Promise<any> {
|
||||
try {
|
||||
const serializeResponse = await this._session.send('Runtime.callFunctionOn', {
|
||||
@ -181,7 +181,7 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
|
||||
}
|
||||
|
||||
async getProperties(handle: js.JSHandle): Promise<Map<string, js.JSHandle>> {
|
||||
const objectId = toRemoteObject(handle).objectId;
|
||||
const objectId = handle._remoteObject.objectId;
|
||||
if (!objectId)
|
||||
return new Map();
|
||||
const response = await this._session.send('Runtime.getProperties', {
|
||||
@ -198,11 +198,11 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
|
||||
}
|
||||
|
||||
async releaseHandle(handle: js.JSHandle): Promise<void> {
|
||||
await releaseObject(this._session, toRemoteObject(handle));
|
||||
await releaseObject(this._session, handle._remoteObject);
|
||||
}
|
||||
|
||||
async handleJSONValue<T>(handle: js.JSHandle<T>): Promise<T> {
|
||||
const remoteObject = toRemoteObject(handle);
|
||||
const remoteObject = handle._remoteObject;
|
||||
if (remoteObject.objectId) {
|
||||
const response = await this._session.send('Runtime.callFunctionOn', {
|
||||
functionDeclaration: 'function() { return this; }',
|
||||
@ -215,7 +215,7 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
|
||||
}
|
||||
|
||||
handleToString(handle: js.JSHandle, includeType: boolean): string {
|
||||
const object = toRemoteObject(handle);
|
||||
const object = handle._remoteObject as Protocol.Runtime.RemoteObject;
|
||||
if (object.objectId) {
|
||||
let type: string = object.subtype || object.type;
|
||||
// FIXME: promise doesn't have special subtype in WebKit.
|
||||
@ -233,7 +233,3 @@ const contextDestroyedResult = {
|
||||
description: 'Protocol error: Execution context was destroyed, most likely because of a navigation.'
|
||||
} as Protocol.Runtime.RemoteObject
|
||||
};
|
||||
|
||||
function toRemoteObject(handle: js.JSHandle): Protocol.Runtime.RemoteObject {
|
||||
return handle._remoteObject as Protocol.Runtime.RemoteObject;
|
||||
}
|
||||
|
||||
@ -18,8 +18,10 @@
|
||||
import { assert } from '../helper';
|
||||
import { WKSession } from './wkConnection';
|
||||
import { Protocol } from './protocol';
|
||||
import * as js from '../javascript';
|
||||
|
||||
export function valueFromRemoteObject(remoteObject: Protocol.Runtime.RemoteObject): any {
|
||||
export function valueFromRemoteObject(ro: js.RemoteObject): any {
|
||||
const remoteObject = ro as Protocol.Runtime.RemoteObject;
|
||||
assert(!remoteObject.objectId, 'Cannot extract value when objectId is given');
|
||||
if (remoteObject.type === 'number') {
|
||||
if (remoteObject.value === null) {
|
||||
@ -43,7 +45,7 @@ export function valueFromRemoteObject(remoteObject: Protocol.Runtime.RemoteObjec
|
||||
return remoteObject.value;
|
||||
}
|
||||
|
||||
export async function releaseObject(client: WKSession, remoteObject: Protocol.Runtime.RemoteObject) {
|
||||
export async function releaseObject(client: WKSession, remoteObject: js.RemoteObject) {
|
||||
if (!remoteObject.objectId)
|
||||
return;
|
||||
await client.send('Runtime.releaseObject', {objectId: remoteObject.objectId}).catch(error => {});
|
||||
|
||||
@ -33,4 +33,16 @@ describe('Capabilities', function() {
|
||||
}, server.PORT);
|
||||
expect(value).toBe('incoming');
|
||||
});
|
||||
|
||||
it.fail(FFOX)('should respect CSP', async({page, server}) => {
|
||||
server.setCSP('/empty.html', 'script-src ' + server.PREFIX);
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
expect(await page.evaluate(() => new Promise(f => setTimeout(() => {
|
||||
try {
|
||||
f(eval("'failed'"));
|
||||
} catch (e) {
|
||||
f('success');
|
||||
}
|
||||
}, 0)))).toBe('success');
|
||||
});
|
||||
});
|
||||
|
||||
@ -300,6 +300,32 @@ describe('Page.evaluate', function() {
|
||||
await page.goto(server.PREFIX + '/empty.html');
|
||||
expect(await page.evaluate(() => new Function('return true')())).toBe(true);
|
||||
});
|
||||
it('should work with non-strict expressions', async({page, server}) => {
|
||||
expect(await page.evaluate(() => {
|
||||
y = 3.14;
|
||||
return y;
|
||||
})).toBe(3.14);
|
||||
});
|
||||
it('should respect use strict expression', async({page, server}) => {
|
||||
const error = await page.evaluate(() => {
|
||||
"use strict";
|
||||
variableY = 3.14;
|
||||
return variableY;
|
||||
}).catch(e => e);
|
||||
expect(error.message).toContain('variableY');
|
||||
});
|
||||
it('should not leak utility script', async({page, server}) => {
|
||||
expect(await page.evaluate(() => this === window)).toBe(true);
|
||||
});
|
||||
it('should not leak handles', async({page, server}) => {
|
||||
const error = await page.evaluate(() => handles.length).catch(e => e);
|
||||
expect(error.message).toContain(' handles');
|
||||
});
|
||||
it('should work with CSP', async({page, server}) => {
|
||||
server.setCSP('/empty.html', `script-src 'self'`);
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
expect(await page.evaluate(() => 2 + 2)).toBe(4);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Page.addInitScript', function() {
|
||||
|
||||
@ -19,6 +19,7 @@ const path = require('path');
|
||||
|
||||
const files = [
|
||||
path.join('src', 'injected', 'injectedScript.webpack.config.js'),
|
||||
path.join('src', 'injected', 'utilityScript.webpack.config.js'),
|
||||
];
|
||||
|
||||
function runOne(runner, file) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user