mirror of
https://github.com/microsoft/playwright.git
synced 2025-01-03 00:37:50 +03:00
api(eval): allow non-toplevel handles as eval arguments (#1404)
This commit is contained in:
parent
045277d5cd
commit
dd850ada89
@ -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",
|
||||
|
@ -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<Protocol.Runtime.CallArgument>(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'}};
|
||||
|
@ -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<Protocol.Runtime.CallFunctionArgument>(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);
|
||||
|
@ -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<any>;
|
||||
@ -102,3 +103,110 @@ export class JSHandle<T = any> {
|
||||
return this._context._delegate.handleToString(this, true /* includeType */);
|
||||
}
|
||||
}
|
||||
|
||||
export function prepareFunctionCall<T>(
|
||||
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<any>();
|
||||
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 };
|
||||
}
|
||||
|
@ -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<T>(url: string, onopen: (transport: ConnectionTransport) => Promise<T>): Promise<T> {
|
||||
|
@ -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<V> {
|
||||
|
||||
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<number>();
|
||||
const pendingBrowserContextDeletions = new Map<number, string>();
|
||||
|
@ -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',
|
||||
|
@ -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<Protocol.Runtime.RemoteObjectId>;
|
||||
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<any> {
|
||||
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<any> {
|
||||
private async _evaluateRemoteObject(context: js.ExecutionContext, pageFunction: Function | string, args: any[]): Promise<any> {
|
||||
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<MaybeCallArgument>(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<Protocol.Runtime.RemoteObjectId> {
|
||||
@ -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}`;
|
||||
|
@ -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;
|
||||
|
@ -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 = [];
|
||||
|
Loading…
Reference in New Issue
Block a user