From dd850ada89e31122f56e2a6ac741d21c23e4c101 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Wed, 18 Mar 2020 10:41:46 -0700 Subject: [PATCH] api(eval): allow non-toplevel handles as eval arguments (#1404) --- package.json | 2 - src/chromium/crExecutionContext.ts | 68 ++++++---------- src/firefox/ffExecutionContext.ts | 51 ++++-------- src/javascript.ts | 108 +++++++++++++++++++++++++ src/platform.ts | 9 +++ src/server/webkit.ts | 3 +- src/web.webpack.config.js | 6 +- src/webkit/wkExecutionContext.ts | 122 ++++++++++------------------- test/jshandle.spec.js | 71 +++++++++++++++-- test/web.spec.js | 8 ++ 10 files changed, 274 insertions(+), 174 deletions(-) diff --git a/package.json b/package.json index 07139f9fce..dcce29d197 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,6 @@ "progress": "^2.0.3", "proxy-from-env": "^1.1.0", "rimraf": "^3.0.2", - "uuid": "^3.4.0", "ws": "^6.1.0" }, "devDependencies": { @@ -60,7 +59,6 @@ "@types/pngjs": "^3.4.0", "@types/proxy-from-env": "^1.0.0", "@types/rimraf": "^2.0.2", - "@types/uuid": "^3.4.6", "@types/ws": "^6.0.1", "@typescript-eslint/eslint-plugin": "^2.6.1", "@typescript-eslint/parser": "^2.6.1", diff --git a/src/chromium/crExecutionContext.ts b/src/chromium/crExecutionContext.ts index f55684db54..d73cbde660 100644 --- a/src/chromium/crExecutionContext.ts +++ b/src/chromium/crExecutionContext.ts @@ -55,28 +55,35 @@ export class CRExecutionContext implements js.ExecutionContextDelegate { if (typeof pageFunction !== 'function') throw new Error(`Expected to get |string| or |function| as the first argument, but got "${pageFunction}" instead.`); - let functionText = pageFunction.toString(); - try { - new Function('(' + functionText + ')'); - } catch (e1) { - // This means we might have a function shorthand. Try another - // time prefixing 'function '. - if (functionText.startsWith('async ')) - functionText = 'async function ' + functionText.substring('async '.length); - else - functionText = 'function ' + functionText; - try { - new Function('(' + functionText + ')'); - } catch (e2) { - // We tried hard to serialize, but there's a weird beast here. - throw new Error('Passed function is not well-serializable!'); + const { functionText, values, handles } = js.prepareFunctionCall(pageFunction, context, args, (value: any) => { + if (typeof value === 'bigint') // eslint-disable-line valid-typeof + return { handle: { unserializableValue: `${value.toString()}n` } }; + if (Object.is(value, -0)) + return { handle: { unserializableValue: '-0' } }; + if (Object.is(value, Infinity)) + return { handle: { unserializableValue: 'Infinity' } }; + if (Object.is(value, -Infinity)) + return { handle: { unserializableValue: '-Infinity' } }; + if (Object.is(value, NaN)) + return { handle: { unserializableValue: 'NaN' } }; + if (value && (value instanceof js.JSHandle)) { + const remoteObject = toRemoteObject(value); + if (remoteObject.unserializableValue) + return { handle: { unserializableValue: remoteObject.unserializableValue } }; + if (!remoteObject.objectId) + return { value: remoteObject.value }; + return { handle: { objectId: remoteObject.objectId } }; } - } + return { value }; + }); const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.callFunctionOn', { functionDeclaration: functionText + '\n' + suffix + '\n', executionContextId: this._contextId, - arguments: args.map(convertArgument.bind(this)), + arguments: [ + ...values.map(value => ({ value })), + ...handles, + ], returnByValue, awaitPromise: true, userGesture: true @@ -85,33 +92,6 @@ export class CRExecutionContext implements js.ExecutionContextDelegate { throw new Error('Evaluation failed: ' + getExceptionMessage(exceptionDetails)); return returnByValue ? valueFromRemoteObject(remoteObject) : context._createHandle(remoteObject); - function convertArgument(arg: any): any { - if (typeof arg === 'bigint') // eslint-disable-line valid-typeof - return { unserializableValue: `${arg.toString()}n` }; - if (Object.is(arg, -0)) - return { unserializableValue: '-0' }; - if (Object.is(arg, Infinity)) - return { unserializableValue: 'Infinity' }; - if (Object.is(arg, -Infinity)) - return { unserializableValue: '-Infinity' }; - if (Object.is(arg, NaN)) - return { unserializableValue: 'NaN' }; - const objectHandle = arg && (arg instanceof js.JSHandle) ? arg : null; - if (objectHandle) { - if (objectHandle._context !== context) - throw new Error('JSHandles can be evaluated only in the context they were created!'); - if (objectHandle._disposed) - throw new Error('JSHandle is disposed!'); - const remoteObject = toRemoteObject(objectHandle); - if (remoteObject.unserializableValue) - return { unserializableValue: remoteObject.unserializableValue }; - if (!remoteObject.objectId) - return { value: remoteObject.value }; - return { objectId: remoteObject.objectId }; - } - return { value: arg }; - } - function rewriteError(error: Error): Protocol.Runtime.evaluateReturnValue { if (error.message.includes('Object reference chain is too long')) return {result: {type: 'undefined'}}; diff --git a/src/firefox/ffExecutionContext.ts b/src/firefox/ffExecutionContext.ts index c7db7ca2fa..47c650979d 100644 --- a/src/firefox/ffExecutionContext.ts +++ b/src/firefox/ffExecutionContext.ts @@ -44,45 +44,26 @@ export class FFExecutionContext implements js.ExecutionContextDelegate { if (typeof pageFunction !== 'function') throw new Error(`Expected to get |string| or |function| as the first argument, but got "${pageFunction}" instead.`); - let functionText = pageFunction.toString(); - try { - new Function('(' + functionText + ')'); - } catch (e1) { - // This means we might have a function shorthand. Try another - // time prefixing 'function '. - if (functionText.startsWith('async ')) - functionText = 'async function ' + functionText.substring('async '.length); - else - functionText = 'function ' + functionText; - try { - new Function('(' + functionText + ')'); - } catch (e2) { - // We tried hard to serialize, but there's a weird beast here. - throw new Error('Passed function is not well-serializable!'); - } - } - const protocolArgs = args.map(arg => { - if (arg instanceof js.JSHandle) { - if (arg._context !== context) - throw new Error('JSHandles can be evaluated only in the context they were created!'); - if (arg._disposed) - throw new Error('JSHandle is disposed!'); - return this._toCallArgument(arg._remoteObject); - } - if (Object.is(arg, Infinity)) - return {unserializableValue: 'Infinity'}; - if (Object.is(arg, -Infinity)) - return {unserializableValue: '-Infinity'}; - if (Object.is(arg, -0)) - return {unserializableValue: '-0'}; - if (Object.is(arg, NaN)) - return {unserializableValue: 'NaN'}; - return {value: arg}; + const { functionText, values, handles } = js.prepareFunctionCall(pageFunction, context, args, (value: any) => { + if (Object.is(value, -0)) + return { handle: { unserializableValue: '-0' } }; + if (Object.is(value, Infinity)) + return { handle: { unserializableValue: 'Infinity' } }; + if (Object.is(value, -Infinity)) + return { handle: { unserializableValue: '-Infinity' } }; + if (Object.is(value, NaN)) + return { handle: { unserializableValue: 'NaN' } }; + if (value && (value instanceof js.JSHandle)) + return { handle: this._toCallArgument(value._remoteObject) }; + return { value }; }); const payload = await this._session.send('Runtime.callFunction', { functionDeclaration: functionText, - args: protocolArgs, + args: [ + ...values.map(value => ({ value })), + ...handles, + ], returnByValue, executionContextId: this._executionContextId }).catch(rewriteError); diff --git a/src/javascript.ts b/src/javascript.ts index 3fda16a140..aa9bf4fe8b 100644 --- a/src/javascript.ts +++ b/src/javascript.ts @@ -16,6 +16,7 @@ import * as types from './types'; import * as dom from './dom'; +import * as platform from './platform'; export interface ExecutionContextDelegate { evaluate(context: ExecutionContext, returnByValue: boolean, pageFunction: string | Function, ...args: any[]): Promise; @@ -102,3 +103,110 @@ export class JSHandle { return this._context._delegate.handleToString(this, true /* includeType */); } } + +export function prepareFunctionCall( + pageFunction: Function, + context: ExecutionContext, + args: any[], + toCallArgumentIfNeeded: (value: any) => { handle?: T, value?: any }): { functionText: string, values: any[], handles: T[] } { + + let functionText = pageFunction.toString(); + try { + new Function('(' + functionText + ')'); + } catch (e1) { + // This means we might have a function shorthand. Try another + // time prefixing 'function '. + if (functionText.startsWith('async ')) + functionText = 'async function ' + functionText.substring('async '.length); + else + functionText = 'function ' + functionText; + try { + new Function('(' + functionText + ')'); + } catch (e2) { + // We tried hard to serialize, but there's a weird beast here. + throw new Error('Passed function is not well-serializable!'); + } + } + + const guids: string[] = []; + const handles: T[] = []; + const pushHandle = (handle: T): string => { + const guid = platform.guid(); + guids.push(guid); + handles.push(handle); + return guid; + }; + + const visited = new Set(); + let error: string | undefined; + const visit = (arg: any, depth: number): any => { + if (!depth) { + error = 'Argument nesting is too deep'; + return; + } + if (visited.has(arg)) { + error = 'Argument is a circular structure'; + return; + } + if (Array.isArray(arg)) { + visited.add(arg); + const result = []; + for (let i = 0; i < arg.length; ++i) + result.push(visit(arg[i], depth - 1)); + visited.delete(arg); + return result; + } + if (arg && (typeof arg === 'object') && !(arg instanceof JSHandle)) { + visited.add(arg); + const result: any = {}; + for (const name of Object.keys(arg)) + result[name] = visit(arg[name], depth - 1); + visited.delete(arg); + return result; + } + if (arg && (arg instanceof JSHandle)) { + if (arg._context !== context) + throw new Error('JSHandles can be evaluated only in the context they were created!'); + if (arg._disposed) + throw new Error('JSHandle is disposed!'); + } + const { handle, value } = toCallArgumentIfNeeded(arg); + if (handle) + return pushHandle(handle); + return value; + }; + + args = args.map(arg => visit(arg, 100)); + if (error) + throw new Error(error); + + if (!guids.length) + return { functionText, values: args, handles: [] }; + + functionText = `(...__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; + })()); + }`; + + return { functionText, values: [ args.length, ...args, guids.length, ...guids ], handles }; +} diff --git a/src/platform.ts b/src/platform.ts index ad02c3638e..5f5253b2a9 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -26,6 +26,7 @@ import * as png from 'pngjs'; import * as http from 'http'; import * as https from 'https'; import * as NodeWebSocket from 'ws'; +import * as crypto from 'crypto'; import { assert, helper } from './helper'; import { ConnectionTransport } from './transport'; @@ -300,6 +301,14 @@ export function makeWaitForNextTask() { }; } +export function guid(): string { + if (isNode) + return crypto.randomBytes(16).toString('hex'); + const a = new Uint8Array(16); + window.crypto.getRandomValues(a); + return Array.from(a).map(b => b.toString(16).padStart(2, '0')).join(''); +} + // 'onmessage' handler must be installed synchronously when 'onopen' callback is invoked to // avoid missing incoming messages. export async function connectToWebsocket(url: string, onopen: (transport: ConnectionTransport) => Promise): Promise { diff --git a/src/server/webkit.ts b/src/server/webkit.ts index b6997db3d3..578e7eaba9 100644 --- a/src/server/webkit.ts +++ b/src/server/webkit.ts @@ -30,7 +30,6 @@ import { kBrowserCloseMessageId } from '../webkit/wkConnection'; import { LaunchOptions, BrowserArgOptions, BrowserType } from './browserType'; import { ConnectionTransport } from '../transport'; import * as ws from 'ws'; -import * as uuidv4 from 'uuid/v4'; import { ConnectOptions, LaunchType } from '../browser'; import { BrowserServer } from './browserServer'; import { Events } from '../events'; @@ -263,7 +262,7 @@ class SequenceNumberMixer { function wrapTransportWithWebSocket(transport: ConnectionTransport, port: number) { const server = new ws.Server({ port }); - const guid = uuidv4(); + const guid = platform.guid(); const idMixer = new SequenceNumberMixer<{id: number, socket: ws}>(); const pendingBrowserContextCreations = new Set(); const pendingBrowserContextDeletions = new Map(); diff --git a/src/web.webpack.config.js b/src/web.webpack.config.js index 8963d5eebd..32e6fb8fd7 100644 --- a/src/web.webpack.config.js +++ b/src/web.webpack.config.js @@ -27,7 +27,10 @@ module.exports = { options: { transpileOnly: true }, - exclude: /node_modules/ + exclude: [ + /node_modules/, + /crypto/, + ] } ] }, @@ -41,6 +44,7 @@ module.exports = { path: path.resolve(__dirname, '../') }, externals: { + 'crypto': 'dummy', 'events': 'dummy', 'fs': 'dummy', 'path': 'dummy', diff --git a/src/webkit/wkExecutionContext.ts b/src/webkit/wkExecutionContext.ts index 28b5c9012d..107c69febf 100644 --- a/src/webkit/wkExecutionContext.ts +++ b/src/webkit/wkExecutionContext.ts @@ -24,6 +24,8 @@ import * as js from '../javascript'; export const EVALUATION_SCRIPT_URL = '__playwright_evaluation_script__'; const SOURCE_URL_REGEX = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m; +type MaybeCallArgument = Protocol.Runtime.CallArgument | { unserializable: any }; + export class WKExecutionContext implements js.ExecutionContextDelegate { private _globalObjectIdPromise?: Promise; private readonly _session: WKSession; @@ -45,7 +47,7 @@ export class WKExecutionContext implements js.ExecutionContextDelegate { async evaluate(context: js.ExecutionContext, returnByValue: boolean, pageFunction: Function | string, ...args: any[]): Promise { try { - let response = await this._evaluateRemoteObject(pageFunction, args); + let response = await this._evaluateRemoteObject(context, pageFunction, args); if (response.result.type === 'object' && response.result.className === 'Promise') { response = await Promise.race([ this._executionContextDestroyedPromise.then(() => contextDestroyedResult), @@ -69,7 +71,7 @@ export class WKExecutionContext implements js.ExecutionContextDelegate { } } - private async _evaluateRemoteObject(pageFunction: Function | string, args: any[]): Promise { + private async _evaluateRemoteObject(context: js.ExecutionContext, pageFunction: Function | string, args: any[]): Promise { if (helper.isString(pageFunction)) { const contextId = this._contextId; const expression: string = pageFunction; @@ -85,60 +87,50 @@ export class WKExecutionContext implements js.ExecutionContextDelegate { if (typeof pageFunction !== 'function') throw new Error(`Expected to get |string| or |function| as the first argument, but got "${pageFunction}" instead.`); - try { - const callParams = this._serializeFunctionAndArguments(pageFunction, args); - const thisObjectId = await this._contextGlobalObjectId(); - return await this._session.send('Runtime.callFunctionOn', { - functionDeclaration: callParams.functionText + '\n' + suffix + '\n', - objectId: thisObjectId, - arguments: callParams.callArguments, - returnByValue: false, - emulateUserGesture: true - }); - } catch (err) { - if (err instanceof TypeError && err.message.startsWith('Converting circular structure to JSON')) - err.message += ' Are you passing a nested JSHandle?'; - throw err; - } + const { functionText, values, handles } = js.prepareFunctionCall(pageFunction, context, args, (value: any) => { + 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); + if (!remoteObject.objectId && !Object.is(valueFromRemoteObject(remoteObject), remoteObject.value)) + return { handle: { unserializable: value } }; + if (!remoteObject.objectId) + return { value: valueFromRemoteObject(remoteObject) }; + return { handle: { objectId: remoteObject.objectId } }; + } + return { value }; + }); + + const callParams = this._serializeFunctionAndArguments(functionText, values, handles); + const thisObjectId = await this._contextGlobalObjectId(); + return await this._session.send('Runtime.callFunctionOn', { + functionDeclaration: callParams.functionText + '\n' + suffix + '\n', + objectId: thisObjectId, + arguments: callParams.callArguments, + returnByValue: false, + emulateUserGesture: true + }); } - private _serializeFunctionAndArguments(pageFunction: Function, args: any[]): { functionText: string, callArguments: Protocol.Runtime.CallArgument[] } { - let functionText = pageFunction.toString(); - try { - new Function('(' + functionText + ')'); - } catch (e1) { - // This means we might have a function shorthand. Try another - // time prefixing 'function '. - if (functionText.startsWith('async ')) - functionText = 'async function ' + functionText.substring('async '.length); - else - functionText = 'function ' + functionText; - try { - new Function('(' + functionText + ')'); - } catch (e2) { - // We tried hard to serialize, but there's a weird beast here. - throw new Error('Passed function is not well-serializable!'); - } - } - - let serializableArgs; - if (args.some(isUnserializable)) { - serializableArgs = []; + private _serializeFunctionAndArguments(functionText: string, values: any[], handles: MaybeCallArgument[]): { functionText: string, callArguments: Protocol.Runtime.CallArgument[] } { + const callArguments: Protocol.Runtime.CallArgument[] = values.map(value => ({ value })); + if (handles.some(handle => 'unserializable' in handle)) { const paramStrings = []; - for (const arg of args) { - if (isUnserializable(arg)) { - paramStrings.push(unserializableToString(arg)); + for (let i = 0; i < callArguments.length; i++) + paramStrings.push('a[' + i + ']'); + for (const handle of handles) { + if ('unserializable' in handle) { + paramStrings.push(unserializableToString(handle.unserializable)); } else { - paramStrings.push('arguments[' + serializableArgs.length + ']'); - serializableArgs.push(arg); + paramStrings.push('a[' + callArguments.length + ']'); + callArguments.push(handle); } } - functionText = `() => (${functionText})(${paramStrings.join(',')})`; + functionText = `(...a) => (${functionText})(${paramStrings.join(',')})`; } else { - serializableArgs = args; + callArguments.push(...(handles as Protocol.Runtime.CallArgument[])); } - const serialized = serializableArgs.map((arg: any) => this._convertArgument(arg)); - return { functionText, callArguments: serialized }; + return { functionText, callArguments }; function unserializableToString(arg: any) { if (Object.is(arg, -0)) @@ -156,25 +148,6 @@ export class WKExecutionContext implements js.ExecutionContextDelegate { } throw new Error('Unsupported value: ' + arg + ' (' + (typeof arg) + ')'); } - - function isUnserializable(arg: any) { - if (typeof arg === 'bigint') - return true; - if (Object.is(arg, -0)) - return true; - if (Object.is(arg, Infinity)) - return true; - if (Object.is(arg, -Infinity)) - return true; - if (Object.is(arg, NaN)) - return true; - if (arg instanceof js.JSHandle) { - const remoteObj = toRemoteObject(arg); - if (!remoteObj.objectId) - return !Object.is(valueFromRemoteObject(remoteObj), remoteObj.value); - } - return false; - } } private _contextGlobalObjectId(): Promise { @@ -252,21 +225,6 @@ export class WKExecutionContext implements js.ExecutionContextDelegate { } return (includeType ? 'JSHandle:' : '') + valueFromRemoteObject(object); } - - private _convertArgument(arg: js.JSHandle | any): Protocol.Runtime.CallArgument { - const objectHandle = arg && (arg instanceof js.JSHandle) ? arg : null; - if (objectHandle) { - if (objectHandle._context._delegate !== this) - throw new Error('JSHandles can be evaluated only in the context they were created!'); - if (objectHandle._disposed) - throw new Error('JSHandle is disposed!'); - const remoteObject = toRemoteObject(arg); - if (!remoteObject.objectId) - return { value: valueFromRemoteObject(remoteObject) }; - return { objectId: remoteObject.objectId }; - } - return { value: arg }; - } } const suffix = `//# sourceURL=${EVALUATION_SCRIPT_URL}`; diff --git a/test/jshandle.spec.js b/test/jshandle.spec.js index 399cb66461..3395f398c3 100644 --- a/test/jshandle.spec.js +++ b/test/jshandle.spec.js @@ -38,19 +38,74 @@ module.exports.describe = function({testRunner, expect, CHROMIUM, FFOX, WEBKIT}) const isFive = await page.evaluate(e => Object.is(e, 5), aHandle); expect(isFive).toBeTruthy(); }); - it('should warn on nested object handles', async({page, server}) => { - const aHandle = await page.evaluateHandle(() => document.body); - let error = null; - await page.evaluateHandle( - opts => opts.elem.querySelector('p'), - { elem: aHandle } - ).catch(e => error = e); - expect(error.message).toContain('Are you passing a nested JSHandle?'); + it('should accept nested handle', async({page, server}) => { + const foo = await page.evaluateHandle(() => ({ x: 1, y: 'foo' })); + const result = await page.evaluate(({ foo }) => { + return foo; + }, { foo }); + expect(result).toEqual({ x: 1, y: 'foo' }); + }); + it('should accept nested window handle', async({page, server}) => { + const foo = await page.evaluateHandle(() => window); + const result = await page.evaluate(({ foo }) => { + return foo === window; + }, { foo }); + expect(result).toBe(true); + }); + it('should accept multiple nested handles', async({page, server}) => { + const foo = await page.evaluateHandle(() => ({ x: 1, y: 'foo' })); + const bar = await page.evaluateHandle(() => 5); + const baz = await page.evaluateHandle(() => (['baz'])); + const result = await page.evaluate((a1, a2) => { + return JSON.stringify({ a1, a2 }); + }, { foo }, { bar, arr: [{ baz }] }); + expect(JSON.parse(result)).toEqual({ + a1: { foo: { x: 1, y: 'foo' } }, + a2: { bar: 5, arr: [{ baz: ['baz'] }] } + }); + }); + it('should throw for deep objects', async({page, server}) => { + let a = { x: 1 }; + for (let i = 0; i < 98; i++) + a = { x: a }; + expect(await page.evaluate(x => x, a)).toEqual(a); + let error = await page.evaluate(x => x, {a}).catch(e => e); + expect(error.message).toBe('Argument nesting is too deep'); + error = await page.evaluate(x => x, [a]).catch(e => e); + expect(error.message).toBe('Argument nesting is too deep'); + }); + it('should throw for circular objects', async({page, server}) => { + const a = { x: 1 }; + a.y = a; + const error = await page.evaluate(x => x, a).catch(e => e); + expect(error.message).toBe('Argument is a circular structure'); + }); + it('should accept same handle multiple times', async({page, server}) => { + const foo = await page.evaluateHandle(() => 1); + expect(await page.evaluate(x => x, { foo, bar: [foo], baz: { foo }})).toEqual({ foo: 1, bar: [1], baz: { foo: 1 } }); + }); + it('should accept same nested object multiple times', async({page, server}) => { + const foo = { x: 1 }; + expect(await page.evaluate(x => x, { foo, bar: [foo], baz: { foo }})).toEqual({ foo: { x: 1 }, bar: [{ x : 1 }], baz: { foo: { x : 1 } } }); }); it('should accept object handle to unserializable value', async({page, server}) => { const aHandle = await page.evaluateHandle(() => Infinity); expect(await page.evaluate(e => Object.is(e, Infinity), aHandle)).toBe(true); }); + it.fail(FFOX)('should pass configurable args', async({page, server}) => { + const result = await page.evaluate(arg => { + if (arg.foo !== 42) + throw new Error('Not a 42'); + arg.foo = 17; + if (arg.foo !== 17) + throw new Error('Not 17'); + delete arg.foo; + if (arg.foo === 17) + throw new Error('Still 17'); + return arg; + }, { foo: 42 }); + expect(result).toEqual({}); + }); it('should use the same JS wrappers', async({page, server}) => { const aHandle = await page.evaluateHandle(() => { window.FOO = 123; diff --git a/test/web.spec.js b/test/web.spec.js index eb72d4a38f..6320bf87a4 100644 --- a/test/web.spec.js +++ b/test/web.spec.js @@ -58,6 +58,14 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, b expect(url).toBe(server.EMPTY_PAGE); }); + it('should evaluate handles', async({page, server}) => { + const foo = await page.evaluateHandle(() => ({ x: 1, y: 'foo' })); + const result = await page.evaluate(({ foo }) => { + return foo; + }, { foo }); + expect(result).toEqual({ x: 1, y: 'foo' }); + }); + it('should receive events', async({page, server}) => { const logs = await page.evaluate(async () => { const logs = [];