chore: introduce utility script for evaluate helpers (#2306)

This commit is contained in:
Pavel Feldman 2020-05-20 15:55:33 -07:00 committed by GitHub
parent d99ebc9265
commit aa0d844c76
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 242 additions and 117 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

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

View 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')),
]
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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