diff --git a/src/chromium/crExecutionContext.ts b/src/chromium/crExecutionContext.ts index 9a32521864..3e82a05a83 100644 --- a/src/chromium/crExecutionContext.ts +++ b/src/chromium/crExecutionContext.ts @@ -30,6 +30,16 @@ export class CRExecutionContext implements js.ExecutionContextDelegate { this._contextId = contextPayload.id; } + async rawEvaluate(expression: string): Promise { + 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 { 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> { @@ -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; +} diff --git a/src/chromium/crPage.ts b/src/chromium/crPage.ts index 858c677e08..526e90e4ad 100644 --- a/src/chromium/crPage.ts +++ b/src/chromium/crPage.ts @@ -760,7 +760,7 @@ class FrameSession { async _getContentFrame(handle: dom.ElementHandle): Promise { 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 { 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 { 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 { 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(handle: dom.ElementHandle, to: dom.FrameExecutionContext): Promise> { 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>; } @@ -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 }); diff --git a/src/dom.ts b/src/dom.ts index a1983dea55..215a9e9615 100644 --- a/src/dom.ts +++ b/src/dom.ts @@ -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; } diff --git a/src/firefox/ffExecutionContext.ts b/src/firefox/ffExecutionContext.ts index a432ef5504..366a70581e 100644 --- a/src/firefox/ffExecutionContext.ts +++ b/src/firefox/ffExecutionContext.ts @@ -29,6 +29,16 @@ export class FFExecutionContext implements js.ExecutionContextDelegate { this._executionContextId = executionContextId; } + async rawEvaluate(expression: string): Promise { + 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 { 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> { @@ -113,7 +116,7 @@ export class FFExecutionContext implements js.ExecutionContextDelegate { async handleJSONValue(handle: js.JSHandle): Promise { 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; +} diff --git a/src/firefox/ffPage.ts b/src/firefox/ffPage.ts index b1505db73f..28e889cf56 100644 --- a/src/firefox/ffPage.ts +++ b/src/firefox/ffPage.ts @@ -373,7 +373,7 @@ export class FFPage implements PageDelegate { async getContentFrame(handle: dom.ElementHandle): Promise { 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 { 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 { 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 { 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(handle: dom.ElementHandle, to: dom.FrameExecutionContext): Promise> { 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; -} diff --git a/src/injected/utilityScript.ts b/src/injected/utilityScript.ts new file mode 100644 index 0000000000..b86b7f03fe --- /dev/null +++ b/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); + } +} diff --git a/src/injected/utilityScript.webpack.config.js b/src/injected/utilityScript.webpack.config.js new file mode 100644 index 0000000000..41159620be --- /dev/null +++ b/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')), + ] +}; diff --git a/src/javascript.ts b/src/javascript.ts index 09f3a92d74..b3cf874aa8 100644 --- a/src/javascript.ts +++ b/src/javascript.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; + rawEvaluate(pageFunction: string): Promise; getProperties(handle: JSHandle): Promise>; releaseHandle(handle: JSHandle): Promise; 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 | 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 { + 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 { 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( 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( 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 { +async function generateSourceMapUrl(functionText: string): Promise { 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(); diff --git a/src/webkit/wkExecutionContext.ts b/src/webkit/wkExecutionContext.ts index 06c5548fe6..62396e8b4d 100644 --- a/src/webkit/wkExecutionContext.ts +++ b/src/webkit/wkExecutionContext.ts @@ -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; 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 { + 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 { 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 { - 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 { 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> { - 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 { - await releaseObject(this._session, toRemoteObject(handle)); + await releaseObject(this._session, handle._remoteObject); } async handleJSONValue(handle: js.JSHandle): Promise { - 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; -} diff --git a/src/webkit/wkProtocolHelper.ts b/src/webkit/wkProtocolHelper.ts index ff19da907b..8b61002b47 100644 --- a/src/webkit/wkProtocolHelper.ts +++ b/src/webkit/wkProtocolHelper.ts @@ -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 => {}); diff --git a/test/capabilities.spec.js b/test/capabilities.spec.js index 6ae15ce2ac..ff0bf480af 100644 --- a/test/capabilities.spec.js +++ b/test/capabilities.spec.js @@ -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'); + }); }); diff --git a/test/evaluation.spec.js b/test/evaluation.spec.js index 379c5d8363..9cc99b5247 100644 --- a/test/evaluation.spec.js +++ b/test/evaluation.spec.js @@ -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() { diff --git a/utils/runWebpack.js b/utils/runWebpack.js index d5007cdddc..9018b2f585 100644 --- a/utils/runWebpack.js +++ b/utils/runWebpack.js @@ -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) {