mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00: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",
|
"progress": "^2.0.3",
|
||||||
"proxy-from-env": "^1.1.0",
|
"proxy-from-env": "^1.1.0",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"uuid": "^3.4.0",
|
|
||||||
"ws": "^6.1.0"
|
"ws": "^6.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -60,7 +59,6 @@
|
|||||||
"@types/pngjs": "^3.4.0",
|
"@types/pngjs": "^3.4.0",
|
||||||
"@types/proxy-from-env": "^1.0.0",
|
"@types/proxy-from-env": "^1.0.0",
|
||||||
"@types/rimraf": "^2.0.2",
|
"@types/rimraf": "^2.0.2",
|
||||||
"@types/uuid": "^3.4.6",
|
|
||||||
"@types/ws": "^6.0.1",
|
"@types/ws": "^6.0.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^2.6.1",
|
"@typescript-eslint/eslint-plugin": "^2.6.1",
|
||||||
"@typescript-eslint/parser": "^2.6.1",
|
"@typescript-eslint/parser": "^2.6.1",
|
||||||
|
@ -55,28 +55,35 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
|
|||||||
if (typeof pageFunction !== 'function')
|
if (typeof pageFunction !== 'function')
|
||||||
throw new Error(`Expected to get |string| or |function| as the first argument, but got "${pageFunction}" instead.`);
|
throw new Error(`Expected to get |string| or |function| as the first argument, but got "${pageFunction}" instead.`);
|
||||||
|
|
||||||
let functionText = pageFunction.toString();
|
const { functionText, values, handles } = js.prepareFunctionCall<Protocol.Runtime.CallArgument>(pageFunction, context, args, (value: any) => {
|
||||||
try {
|
if (typeof value === 'bigint') // eslint-disable-line valid-typeof
|
||||||
new Function('(' + functionText + ')');
|
return { handle: { unserializableValue: `${value.toString()}n` } };
|
||||||
} catch (e1) {
|
if (Object.is(value, -0))
|
||||||
// This means we might have a function shorthand. Try another
|
return { handle: { unserializableValue: '-0' } };
|
||||||
// time prefixing 'function '.
|
if (Object.is(value, Infinity))
|
||||||
if (functionText.startsWith('async '))
|
return { handle: { unserializableValue: 'Infinity' } };
|
||||||
functionText = 'async function ' + functionText.substring('async '.length);
|
if (Object.is(value, -Infinity))
|
||||||
else
|
return { handle: { unserializableValue: '-Infinity' } };
|
||||||
functionText = 'function ' + functionText;
|
if (Object.is(value, NaN))
|
||||||
try {
|
return { handle: { unserializableValue: 'NaN' } };
|
||||||
new Function('(' + functionText + ')');
|
if (value && (value instanceof js.JSHandle)) {
|
||||||
} catch (e2) {
|
const remoteObject = toRemoteObject(value);
|
||||||
// We tried hard to serialize, but there's a weird beast here.
|
if (remoteObject.unserializableValue)
|
||||||
throw new Error('Passed function is not well-serializable!');
|
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', {
|
const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.callFunctionOn', {
|
||||||
functionDeclaration: functionText + '\n' + suffix + '\n',
|
functionDeclaration: functionText + '\n' + suffix + '\n',
|
||||||
executionContextId: this._contextId,
|
executionContextId: this._contextId,
|
||||||
arguments: args.map(convertArgument.bind(this)),
|
arguments: [
|
||||||
|
...values.map(value => ({ value })),
|
||||||
|
...handles,
|
||||||
|
],
|
||||||
returnByValue,
|
returnByValue,
|
||||||
awaitPromise: true,
|
awaitPromise: true,
|
||||||
userGesture: true
|
userGesture: true
|
||||||
@ -85,33 +92,6 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
|
|||||||
throw new Error('Evaluation failed: ' + getExceptionMessage(exceptionDetails));
|
throw new Error('Evaluation failed: ' + getExceptionMessage(exceptionDetails));
|
||||||
return returnByValue ? valueFromRemoteObject(remoteObject) : context._createHandle(remoteObject);
|
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 {
|
function rewriteError(error: Error): Protocol.Runtime.evaluateReturnValue {
|
||||||
if (error.message.includes('Object reference chain is too long'))
|
if (error.message.includes('Object reference chain is too long'))
|
||||||
return {result: {type: 'undefined'}};
|
return {result: {type: 'undefined'}};
|
||||||
|
@ -44,45 +44,26 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
|
|||||||
if (typeof pageFunction !== 'function')
|
if (typeof pageFunction !== 'function')
|
||||||
throw new Error(`Expected to get |string| or |function| as the first argument, but got "${pageFunction}" instead.`);
|
throw new Error(`Expected to get |string| or |function| as the first argument, but got "${pageFunction}" instead.`);
|
||||||
|
|
||||||
let functionText = pageFunction.toString();
|
const { functionText, values, handles } = js.prepareFunctionCall<Protocol.Runtime.CallFunctionArgument>(pageFunction, context, args, (value: any) => {
|
||||||
try {
|
if (Object.is(value, -0))
|
||||||
new Function('(' + functionText + ')');
|
return { handle: { unserializableValue: '-0' } };
|
||||||
} catch (e1) {
|
if (Object.is(value, Infinity))
|
||||||
// This means we might have a function shorthand. Try another
|
return { handle: { unserializableValue: 'Infinity' } };
|
||||||
// time prefixing 'function '.
|
if (Object.is(value, -Infinity))
|
||||||
if (functionText.startsWith('async '))
|
return { handle: { unserializableValue: '-Infinity' } };
|
||||||
functionText = 'async function ' + functionText.substring('async '.length);
|
if (Object.is(value, NaN))
|
||||||
else
|
return { handle: { unserializableValue: 'NaN' } };
|
||||||
functionText = 'function ' + functionText;
|
if (value && (value instanceof js.JSHandle))
|
||||||
try {
|
return { handle: this._toCallArgument(value._remoteObject) };
|
||||||
new Function('(' + functionText + ')');
|
return { value };
|
||||||
} 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 payload = await this._session.send('Runtime.callFunction', {
|
const payload = await this._session.send('Runtime.callFunction', {
|
||||||
functionDeclaration: functionText,
|
functionDeclaration: functionText,
|
||||||
args: protocolArgs,
|
args: [
|
||||||
|
...values.map(value => ({ value })),
|
||||||
|
...handles,
|
||||||
|
],
|
||||||
returnByValue,
|
returnByValue,
|
||||||
executionContextId: this._executionContextId
|
executionContextId: this._executionContextId
|
||||||
}).catch(rewriteError);
|
}).catch(rewriteError);
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
import * as types from './types';
|
import * as types from './types';
|
||||||
import * as dom from './dom';
|
import * as dom from './dom';
|
||||||
|
import * as platform from './platform';
|
||||||
|
|
||||||
export interface ExecutionContextDelegate {
|
export interface ExecutionContextDelegate {
|
||||||
evaluate(context: ExecutionContext, returnByValue: boolean, pageFunction: string | Function, ...args: any[]): Promise<any>;
|
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 */);
|
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 http from 'http';
|
||||||
import * as https from 'https';
|
import * as https from 'https';
|
||||||
import * as NodeWebSocket from 'ws';
|
import * as NodeWebSocket from 'ws';
|
||||||
|
import * as crypto from 'crypto';
|
||||||
|
|
||||||
import { assert, helper } from './helper';
|
import { assert, helper } from './helper';
|
||||||
import { ConnectionTransport } from './transport';
|
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
|
// 'onmessage' handler must be installed synchronously when 'onopen' callback is invoked to
|
||||||
// avoid missing incoming messages.
|
// avoid missing incoming messages.
|
||||||
export async function connectToWebsocket<T>(url: string, onopen: (transport: ConnectionTransport) => Promise<T>): Promise<T> {
|
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 { LaunchOptions, BrowserArgOptions, BrowserType } from './browserType';
|
||||||
import { ConnectionTransport } from '../transport';
|
import { ConnectionTransport } from '../transport';
|
||||||
import * as ws from 'ws';
|
import * as ws from 'ws';
|
||||||
import * as uuidv4 from 'uuid/v4';
|
|
||||||
import { ConnectOptions, LaunchType } from '../browser';
|
import { ConnectOptions, LaunchType } from '../browser';
|
||||||
import { BrowserServer } from './browserServer';
|
import { BrowserServer } from './browserServer';
|
||||||
import { Events } from '../events';
|
import { Events } from '../events';
|
||||||
@ -263,7 +262,7 @@ class SequenceNumberMixer<V> {
|
|||||||
|
|
||||||
function wrapTransportWithWebSocket(transport: ConnectionTransport, port: number) {
|
function wrapTransportWithWebSocket(transport: ConnectionTransport, port: number) {
|
||||||
const server = new ws.Server({ port });
|
const server = new ws.Server({ port });
|
||||||
const guid = uuidv4();
|
const guid = platform.guid();
|
||||||
const idMixer = new SequenceNumberMixer<{id: number, socket: ws}>();
|
const idMixer = new SequenceNumberMixer<{id: number, socket: ws}>();
|
||||||
const pendingBrowserContextCreations = new Set<number>();
|
const pendingBrowserContextCreations = new Set<number>();
|
||||||
const pendingBrowserContextDeletions = new Map<number, string>();
|
const pendingBrowserContextDeletions = new Map<number, string>();
|
||||||
|
@ -27,7 +27,10 @@ module.exports = {
|
|||||||
options: {
|
options: {
|
||||||
transpileOnly: true
|
transpileOnly: true
|
||||||
},
|
},
|
||||||
exclude: /node_modules/
|
exclude: [
|
||||||
|
/node_modules/,
|
||||||
|
/crypto/,
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -41,6 +44,7 @@ module.exports = {
|
|||||||
path: path.resolve(__dirname, '../')
|
path: path.resolve(__dirname, '../')
|
||||||
},
|
},
|
||||||
externals: {
|
externals: {
|
||||||
|
'crypto': 'dummy',
|
||||||
'events': 'dummy',
|
'events': 'dummy',
|
||||||
'fs': 'dummy',
|
'fs': 'dummy',
|
||||||
'path': 'dummy',
|
'path': 'dummy',
|
||||||
|
@ -24,6 +24,8 @@ import * as js from '../javascript';
|
|||||||
export const EVALUATION_SCRIPT_URL = '__playwright_evaluation_script__';
|
export const EVALUATION_SCRIPT_URL = '__playwright_evaluation_script__';
|
||||||
const SOURCE_URL_REGEX = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m;
|
const SOURCE_URL_REGEX = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m;
|
||||||
|
|
||||||
|
type MaybeCallArgument = Protocol.Runtime.CallArgument | { unserializable: any };
|
||||||
|
|
||||||
export class WKExecutionContext implements js.ExecutionContextDelegate {
|
export class WKExecutionContext implements js.ExecutionContextDelegate {
|
||||||
private _globalObjectIdPromise?: Promise<Protocol.Runtime.RemoteObjectId>;
|
private _globalObjectIdPromise?: Promise<Protocol.Runtime.RemoteObjectId>;
|
||||||
private readonly _session: WKSession;
|
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> {
|
async evaluate(context: js.ExecutionContext, returnByValue: boolean, pageFunction: Function | string, ...args: any[]): Promise<any> {
|
||||||
try {
|
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') {
|
if (response.result.type === 'object' && response.result.className === 'Promise') {
|
||||||
response = await Promise.race([
|
response = await Promise.race([
|
||||||
this._executionContextDestroyedPromise.then(() => contextDestroyedResult),
|
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)) {
|
if (helper.isString(pageFunction)) {
|
||||||
const contextId = this._contextId;
|
const contextId = this._contextId;
|
||||||
const expression: string = pageFunction;
|
const expression: string = pageFunction;
|
||||||
@ -85,8 +87,21 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
|
|||||||
if (typeof pageFunction !== 'function')
|
if (typeof pageFunction !== 'function')
|
||||||
throw new Error(`Expected to get |string| or |function| as the first argument, but got "${pageFunction}" instead.`);
|
throw new Error(`Expected to get |string| or |function| as the first argument, but got "${pageFunction}" instead.`);
|
||||||
|
|
||||||
try {
|
const { functionText, values, handles } = js.prepareFunctionCall<MaybeCallArgument>(pageFunction, context, args, (value: any) => {
|
||||||
const callParams = this._serializeFunctionAndArguments(pageFunction, args);
|
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();
|
const thisObjectId = await this._contextGlobalObjectId();
|
||||||
return await this._session.send('Runtime.callFunctionOn', {
|
return await this._session.send('Runtime.callFunctionOn', {
|
||||||
functionDeclaration: callParams.functionText + '\n' + suffix + '\n',
|
functionDeclaration: callParams.functionText + '\n' + suffix + '\n',
|
||||||
@ -95,50 +110,27 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
|
|||||||
returnByValue: false,
|
returnByValue: false,
|
||||||
emulateUserGesture: true
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _serializeFunctionAndArguments(pageFunction: Function, args: any[]): { functionText: string, callArguments: Protocol.Runtime.CallArgument[] } {
|
private _serializeFunctionAndArguments(functionText: string, values: any[], handles: MaybeCallArgument[]): { functionText: string, callArguments: Protocol.Runtime.CallArgument[] } {
|
||||||
let functionText = pageFunction.toString();
|
const callArguments: Protocol.Runtime.CallArgument[] = values.map(value => ({ value }));
|
||||||
try {
|
if (handles.some(handle => 'unserializable' in handle)) {
|
||||||
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 = [];
|
|
||||||
const paramStrings = [];
|
const paramStrings = [];
|
||||||
for (const arg of args) {
|
for (let i = 0; i < callArguments.length; i++)
|
||||||
if (isUnserializable(arg)) {
|
paramStrings.push('a[' + i + ']');
|
||||||
paramStrings.push(unserializableToString(arg));
|
for (const handle of handles) {
|
||||||
|
if ('unserializable' in handle) {
|
||||||
|
paramStrings.push(unserializableToString(handle.unserializable));
|
||||||
} else {
|
} else {
|
||||||
paramStrings.push('arguments[' + serializableArgs.length + ']');
|
paramStrings.push('a[' + callArguments.length + ']');
|
||||||
serializableArgs.push(arg);
|
callArguments.push(handle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
functionText = `() => (${functionText})(${paramStrings.join(',')})`;
|
functionText = `(...a) => (${functionText})(${paramStrings.join(',')})`;
|
||||||
} else {
|
} else {
|
||||||
serializableArgs = args;
|
callArguments.push(...(handles as Protocol.Runtime.CallArgument[]));
|
||||||
}
|
}
|
||||||
const serialized = serializableArgs.map((arg: any) => this._convertArgument(arg));
|
return { functionText, callArguments };
|
||||||
return { functionText, callArguments: serialized };
|
|
||||||
|
|
||||||
function unserializableToString(arg: any) {
|
function unserializableToString(arg: any) {
|
||||||
if (Object.is(arg, -0))
|
if (Object.is(arg, -0))
|
||||||
@ -156,25 +148,6 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
|
|||||||
}
|
}
|
||||||
throw new Error('Unsupported value: ' + arg + ' (' + (typeof arg) + ')');
|
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> {
|
private _contextGlobalObjectId(): Promise<Protocol.Runtime.RemoteObjectId> {
|
||||||
@ -252,21 +225,6 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
|
|||||||
}
|
}
|
||||||
return (includeType ? 'JSHandle:' : '') + valueFromRemoteObject(object);
|
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}`;
|
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);
|
const isFive = await page.evaluate(e => Object.is(e, 5), aHandle);
|
||||||
expect(isFive).toBeTruthy();
|
expect(isFive).toBeTruthy();
|
||||||
});
|
});
|
||||||
it('should warn on nested object handles', async({page, server}) => {
|
it('should accept nested handle', async({page, server}) => {
|
||||||
const aHandle = await page.evaluateHandle(() => document.body);
|
const foo = await page.evaluateHandle(() => ({ x: 1, y: 'foo' }));
|
||||||
let error = null;
|
const result = await page.evaluate(({ foo }) => {
|
||||||
await page.evaluateHandle(
|
return foo;
|
||||||
opts => opts.elem.querySelector('p'),
|
}, { foo });
|
||||||
{ elem: aHandle }
|
expect(result).toEqual({ x: 1, y: 'foo' });
|
||||||
).catch(e => error = e);
|
});
|
||||||
expect(error.message).toContain('Are you passing a nested JSHandle?');
|
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}) => {
|
it('should accept object handle to unserializable value', async({page, server}) => {
|
||||||
const aHandle = await page.evaluateHandle(() => Infinity);
|
const aHandle = await page.evaluateHandle(() => Infinity);
|
||||||
expect(await page.evaluate(e => Object.is(e, Infinity), aHandle)).toBe(true);
|
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}) => {
|
it('should use the same JS wrappers', async({page, server}) => {
|
||||||
const aHandle = await page.evaluateHandle(() => {
|
const aHandle = await page.evaluateHandle(() => {
|
||||||
window.FOO = 123;
|
window.FOO = 123;
|
||||||
|
@ -58,6 +58,14 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, b
|
|||||||
expect(url).toBe(server.EMPTY_PAGE);
|
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}) => {
|
it('should receive events', async({page, server}) => {
|
||||||
const logs = await page.evaluate(async () => {
|
const logs = await page.evaluate(async () => {
|
||||||
const logs = [];
|
const logs = [];
|
||||||
|
Loading…
x
Reference in New Issue
Block a user