fix(evaluate): consistently serialize json values (#2377)

This commit is contained in:
Pavel Feldman 2020-05-27 17:19:05 -07:00 committed by GitHub
parent 609bc4cfb0
commit e168fddac8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 410 additions and 441 deletions

View File

@ -95,8 +95,8 @@ class CRAXNode implements accessibility.AXNode {
}
async _findElement(element: dom.ElementHandle): Promise<CRAXNode | null> {
const remoteObject = element._remoteObject as Protocol.Runtime.RemoteObject;
const {node: {backendNodeId}} = await this._client.send('DOM.describeNode', {objectId: remoteObject.objectId});
const objectId = element._objectId!;
const {node: {backendNodeId}} = await this._client.send('DOM.describeNode', { objectId });
const needle = this.find(node => node._payload.backendDOMNodeId === backendNodeId);
return needle || null;
}

View File

@ -17,10 +17,11 @@
import { CRSession } from './crConnection';
import { helper } from '../helper';
import { valueFromRemoteObject, getExceptionMessage, releaseObject } from './crProtocolHelper';
import { getExceptionMessage, releaseObject } from './crProtocolHelper';
import { Protocol } from './protocol';
import * as js from '../javascript';
import * as debugSupport from '../debug/debugSupport';
import { RemoteObject, parseEvaluationResultValue } from '../remoteObject';
export class CRExecutionContext implements js.ExecutionContextDelegate {
_client: CRSession;
@ -31,7 +32,7 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
this._contextId = contextPayload.id;
}
async rawEvaluate(expression: string): Promise<js.RemoteObject> {
async rawEvaluate(expression: string): Promise<RemoteObject> {
const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.evaluate', {
expression: debugSupport.ensureSourceUrl(expression),
contextId: this._contextId,
@ -51,29 +52,7 @@ 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.`);
const { functionText, values, handles, dispose } = await 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 { handle: { value: remoteObject.value } };
return { handle: { objectId: remoteObject.objectId } };
}
return { value };
});
const { functionText, values, handles, dispose } = await js.prepareFunctionCall(pageFunction, context, args);
return this._callOnUtilityScript(context,
'callFunction', [
{ value: functionText },
@ -87,7 +66,7 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
const utilityScript = await context.utilityScript();
const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.callFunctionOn', {
functionDeclaration: `function (...args) { return this.${method}(...args) }` + debugSupport.generateSourceUrl(),
objectId: utilityScript._remoteObject.objectId,
objectId: utilityScript._objectId,
arguments: [
{ value: returnByValue },
...args
@ -98,14 +77,14 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
}).catch(rewriteError);
if (exceptionDetails)
throw new Error('Evaluation failed: ' + getExceptionMessage(exceptionDetails));
return returnByValue ? valueFromRemoteObject(remoteObject) : context.createHandle(remoteObject);
return returnByValue ? parseEvaluationResultValue(remoteObject.value) : context.createHandle(remoteObject);
} finally {
dispose();
}
}
async getProperties(handle: js.JSHandle): Promise<Map<string, js.JSHandle>> {
const objectId = toRemoteObject(handle).objectId;
const objectId = handle._objectId;
if (!objectId)
return new Map();
const response = await this._client.send('Runtime.getProperties', {
@ -114,7 +93,7 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
});
const result = new Map();
for (const property of response.result) {
if (!property.enumerable)
if (!property.enumerable || !property.value)
continue;
result.set(property.name, handle._context.createHandle(property.value));
}
@ -122,35 +101,20 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
}
async releaseHandle(handle: js.JSHandle): Promise<void> {
await releaseObject(this._client, toRemoteObject(handle));
if (!handle._objectId)
return;
await releaseObject(this._client, handle._objectId);
}
async handleJSONValue<T>(handle: js.JSHandle<T>): Promise<T> {
const remoteObject = toRemoteObject(handle);
if (remoteObject.objectId) {
const response = await this._client.send('Runtime.callFunctionOn', {
functionDeclaration: 'function() { return this; }' + debugSupport.generateSourceUrl(),
objectId: remoteObject.objectId,
returnByValue: true,
awaitPromise: true,
});
return valueFromRemoteObject(response.result);
if (handle._objectId) {
return this._callOnUtilityScript(handle._context,
`jsonValue`, [
{ objectId: handle._objectId },
], true, () => {});
}
return valueFromRemoteObject(remoteObject);
return handle._value;
}
handleToString(handle: js.JSHandle, includeType: boolean): string {
const object = toRemoteObject(handle);
if (object.objectId) {
const type = object.subtype || object.type;
return 'JSHandle@' + type;
}
return (includeType ? 'JSHandle:' : '') + valueFromRemoteObject(object);
}
}
function toRemoteObject(handle: js.JSHandle): Protocol.Runtime.RemoteObject {
return handle._remoteObject as Protocol.Runtime.RemoteObject;
}
function rewriteError(error: Error): Protocol.Runtime.evaluateReturnValue {

View File

@ -665,7 +665,7 @@ class FrameSession {
_onLogEntryAdded(event: Protocol.Log.entryAddedPayload) {
const {level, text, args, source, url, lineNumber} = event.entry;
if (args)
args.map(arg => releaseObject(this._client, arg));
args.map(arg => releaseObject(this._client, arg.objectId!));
if (source !== 'worker')
this._page.emit(Events.Page.Console, new ConsoleMessage(level, text, [], {url, lineNumber}));
}
@ -778,7 +778,7 @@ class FrameSession {
async _getContentFrame(handle: dom.ElementHandle): Promise<frames.Frame | null> {
const nodeInfo = await this._client.send('DOM.describeNode', {
objectId: handle._remoteObject.objectId
objectId: handle._objectId
});
if (!nodeInfo || typeof nodeInfo.node.frameId !== 'string')
return null;
@ -795,11 +795,10 @@ class FrameSession {
});
if (!documentElement)
return null;
const remoteObject = documentElement._remoteObject;
if (!remoteObject.objectId)
if (!documentElement._objectId)
return null;
const nodeInfo = await this._client.send('DOM.describeNode', {
objectId: remoteObject.objectId
objectId: documentElement._objectId
});
const frameId = nodeInfo && typeof nodeInfo.node.frameId === 'string' ?
nodeInfo.node.frameId : null;
@ -809,7 +808,7 @@ class FrameSession {
async _getBoundingBox(handle: dom.ElementHandle): Promise<types.Rect | null> {
const result = await this._client.send('DOM.getBoxModel', {
objectId: handle._remoteObject.objectId
objectId: handle._objectId
}).catch(logError(this._page));
if (!result)
return null;
@ -823,7 +822,7 @@ class FrameSession {
async _scrollRectIntoViewIfNeeded(handle: dom.ElementHandle, rect?: types.Rect): Promise<'success' | 'invisible'> {
return await this._client.send('DOM.scrollIntoViewIfNeeded', {
objectId: handle._remoteObject.objectId,
objectId: handle._objectId,
rect,
}).then(() => 'success' as const).catch(e => {
if (e instanceof Error && e.message.includes('Node does not have a layout object'))
@ -839,7 +838,7 @@ class FrameSession {
async _getContentQuads(handle: dom.ElementHandle): Promise<types.Quad[] | null> {
const result = await this._client.send('DOM.getContentQuads', {
objectId: handle._remoteObject.objectId
objectId: handle._objectId
}).catch(logError(this._page));
if (!result)
return null;
@ -853,7 +852,7 @@ class FrameSession {
async _adoptElementHandle<T extends Node>(handle: dom.ElementHandle<T>, to: dom.FrameExecutionContext): Promise<dom.ElementHandle<T>> {
const nodeInfo = await this._client.send('DOM.describeNode', {
objectId: handle._remoteObject.objectId,
objectId: handle._objectId,
});
return this._adoptBackendNodeId(nodeInfo.node.backendNodeId, to) as Promise<dom.ElementHandle<T>>;
}

View File

@ -15,7 +15,6 @@
* limitations under the License.
*/
import { assert } from '../helper';
import { CRSession } from './crConnection';
import { Protocol } from './protocol';
import * as fs from 'fs';
@ -35,31 +34,8 @@ export function getExceptionMessage(exceptionDetails: Protocol.Runtime.Exception
return message;
}
export function valueFromRemoteObject(remoteObject: Protocol.Runtime.RemoteObject): any {
assert(!remoteObject.objectId, 'Cannot extract value when objectId is given');
if (remoteObject.unserializableValue) {
if (remoteObject.type === 'bigint' && typeof BigInt !== 'undefined')
return BigInt(remoteObject.unserializableValue.replace('n', ''));
switch (remoteObject.unserializableValue) {
case '-0':
return -0;
case 'NaN':
return NaN;
case 'Infinity':
return Infinity;
case '-Infinity':
return -Infinity;
default:
throw new Error('Unsupported unserializable value: ' + remoteObject.unserializableValue);
}
}
return remoteObject.value;
}
export async function releaseObject(client: CRSession, remoteObject: Protocol.Runtime.RemoteObject) {
if (!remoteObject.objectId)
return;
await client.send('Runtime.releaseObject', {objectId: remoteObject.objectId}).catch(error => {});
export async function releaseObject(client: CRSession, objectId: string) {
await client.send('Runtime.releaseObject', { objectId }).catch(error => {});
}
export async function readProtocolStream(client: CRSession, handle: string, path: string | null): Promise<Buffer> {

View File

@ -42,7 +42,7 @@ export class ConsoleMessage {
text(): string {
if (this._text === undefined)
this._text = this._args.map(arg => arg._context._delegate.handleToString(arg, false /* includeType */)).join(' ');
this._text = this._args.map(arg => arg._handleToString(false /* includeType */)).join(' ');
return this._text;
}

View File

@ -21,7 +21,7 @@ import { Protocol } from './protocol';
import * as dom from '../dom';
export async function getAccessibilityTree(session: FFSession, needle?: dom.ElementHandle): Promise<{tree: accessibility.AXNode, needle: accessibility.AXNode | null}> {
const objectId = needle ? needle._remoteObject.objectId : undefined;
const objectId = needle ? needle._objectId : undefined;
const { tree } = await session.send('Accessibility.getFullAXTree', { objectId });
const axNode = new FFAXNode(tree);
return {

View File

@ -20,6 +20,7 @@ import * as js from '../javascript';
import { FFSession } from './ffConnection';
import { Protocol } from './protocol';
import * as debugSupport from '../debug/debugSupport';
import { RemoteObject, parseEvaluationResultValue } from '../remoteObject';
export class FFExecutionContext implements js.ExecutionContextDelegate {
_session: FFSession;
@ -30,7 +31,7 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
this._executionContextId = executionContextId;
}
async rawEvaluate(expression: string): Promise<js.RemoteObject> {
async rawEvaluate(expression: string): Promise<RemoteObject> {
const payload = await this._session.send('Runtime.evaluate', {
expression: debugSupport.ensureSourceUrl(expression),
returnByValue: false,
@ -50,25 +51,13 @@ 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.`);
const { functionText, values, handles, dispose } = await 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 { functionText, values, handles, dispose } = await js.prepareFunctionCall(pageFunction, context, args);
return this._callOnUtilityScript(context,
`callFunction`, [
{ value: functionText },
...values.map(value => ({ value })),
...handles,
...handles.map(handle => ({ objectId: handle.objectId, value: handle.value })),
], returnByValue, dispose);
}
@ -78,7 +67,7 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
const payload = await this._session.send('Runtime.callFunction', {
functionDeclaration: `(utilityScript, ...args) => utilityScript.${method}(...args)` + debugSupport.generateSourceUrl(),
args: [
{ objectId: utilityScript._remoteObject.objectId, value: undefined },
{ objectId: utilityScript._objectId, value: undefined },
{ value: returnByValue },
...args
],
@ -87,15 +76,15 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
}).catch(rewriteError);
checkException(payload.exceptionDetails);
if (returnByValue)
return deserializeValue(payload.result!);
return context.createHandle(payload.result);
return parseEvaluationResultValue(payload.result!.value);
return context.createHandle(payload.result!);
} finally {
dispose();
}
}
async getProperties(handle: js.JSHandle): Promise<Map<string, js.JSHandle>> {
const objectId = handle._remoteObject.objectId;
const objectId = handle._objectId;
if (!objectId)
return new Map();
const response = await this._session.send('Runtime.getObjectProperties', {
@ -109,36 +98,22 @@ export class FFExecutionContext implements js.ExecutionContextDelegate {
}
async releaseHandle(handle: js.JSHandle): Promise<void> {
if (!handle._remoteObject.objectId)
if (!handle._objectId)
return;
await this._session.send('Runtime.disposeObject', {
executionContextId: this._executionContextId,
objectId: handle._remoteObject.objectId,
objectId: handle._objectId,
}).catch(error => {});
}
async handleJSONValue<T>(handle: js.JSHandle<T>): Promise<T> {
const payload = handle._remoteObject;
if (!payload.objectId)
return deserializeValue(payload as Protocol.Runtime.RemoteObject);
const simpleValue = await this._session.send('Runtime.callFunction', {
executionContextId: this._executionContextId,
returnByValue: true,
functionDeclaration: ((e: any) => e).toString() + debugSupport.generateSourceUrl(),
args: [this._toCallArgument(payload)],
});
return deserializeValue(simpleValue.result!);
}
handleToString(handle: js.JSHandle, includeType: boolean): string {
const payload = handle._remoteObject;
if (payload.objectId)
return 'JSHandle@' + (payload.subtype || payload.type);
return (includeType ? 'JSHandle:' : '') + deserializeValue(payload as Protocol.Runtime.RemoteObject);
}
private _toCallArgument(payload: any): any {
return { value: payload.value, unserializableValue: payload.unserializableValue, objectId: payload.objectId };
if (handle._objectId) {
return await this._callOnUtilityScript(handle._context,
`jsonValue`, [
{ objectId: handle._objectId, value: undefined },
], true, () => {});
}
return handle._value;
}
}
@ -151,18 +126,6 @@ function checkException(exceptionDetails?: Protocol.Runtime.ExceptionDetails) {
throw new Error('Evaluation failed: ' + exceptionDetails.text + '\n' + exceptionDetails.stack);
}
export function deserializeValue({unserializableValue, value}: Protocol.Runtime.RemoteObject) {
if (unserializableValue === 'Infinity')
return Infinity;
if (unserializableValue === '-Infinity')
return -Infinity;
if (unserializableValue === '-0')
return -0;
if (unserializableValue === 'NaN')
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}};

View File

@ -373,7 +373,7 @@ export class FFPage implements PageDelegate {
async getContentFrame(handle: dom.ElementHandle): Promise<frames.Frame | null> {
const { contentFrameId } = await this._session.send('Page.describeNode', {
frameId: handle._context.frame._id,
objectId: handle._remoteObject.objectId!,
objectId: handle._objectId!,
});
if (!contentFrameId)
return null;
@ -383,7 +383,7 @@ export class FFPage implements PageDelegate {
async getOwnerFrame(handle: dom.ElementHandle): Promise<string | null> {
const { ownerFrameId } = await this._session.send('Page.describeNode', {
frameId: handle._context.frame._id,
objectId: handle._remoteObject.objectId!,
objectId: handle._objectId!,
});
return ownerFrameId || null;
}
@ -414,7 +414,7 @@ export class FFPage implements PageDelegate {
async scrollRectIntoViewIfNeeded(handle: dom.ElementHandle, rect?: types.Rect): Promise<'success' | 'invisible'> {
return await this._session.send('Page.scrollIntoViewIfNeeded', {
frameId: handle._context.frame._id,
objectId: handle._remoteObject.objectId!,
objectId: handle._objectId!,
rect,
}).then(() => 'success' as const).catch(e => {
if (e instanceof Error && e.message.includes('Node is detached from document'))
@ -433,7 +433,7 @@ export class FFPage implements PageDelegate {
async getContentQuads(handle: dom.ElementHandle): Promise<types.Quad[] | null> {
const result = await this._session.send('Page.getContentQuads', {
frameId: handle._context.frame._id,
objectId: handle._remoteObject.objectId!,
objectId: handle._objectId!,
}).catch(logError(this._page));
if (!result)
return null;
@ -452,7 +452,7 @@ export class FFPage implements PageDelegate {
async adoptElementHandle<T extends Node>(handle: dom.ElementHandle<T>, to: dom.FrameExecutionContext): Promise<dom.ElementHandle<T>> {
const result = await this._session.send('Page.adoptNode', {
frameId: handle._context.frame._id,
objectId: handle._remoteObject.objectId!,
objectId: handle._objectId!,
executionContextId: (to._delegate as FFExecutionContext)._executionContextId
});
if (!result.remoteObject)

View File

@ -14,44 +14,38 @@
* limitations under the License.
*/
import { serializeAsCallArgument, parseEvaluationResultValue } from '../remoteObject';
export default class UtilityScript {
evaluate(returnByValue: boolean, expression: string) {
const result = global.eval(expression);
return returnByValue ? this._serialize(result) : result;
return returnByValue ? this._promiseAwareJsonValueNoThrow(result) : result;
}
callFunction(returnByValue: boolean, 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 handles = args.slice(argCount + 1);
const parameters = args.slice(1, argCount + 1).map(a => parseEvaluationResultValue(a, handles));
const func = global.eval('(' + functionText + ')');
const result = func(...processedArgs);
return returnByValue ? this._serialize(result) : result;
const result = func(...parameters);
return returnByValue ? this._promiseAwareJsonValueNoThrow(result) : result;
}
private _serialize(value: any): any {
if (value instanceof Error) {
const error = value;
if ('captureStackTrace' in global.Error) {
// v8
return error.stack;
jsonValue(returnByValue: true, value: any) {
return serializeAsCallArgument(value, (value: any) => ({ fallThrough: value }));
}
private _promiseAwareJsonValueNoThrow(value: any) {
const safeJson = (value: any) => {
try {
return this.jsonValue(true, value);
} catch (e) {
return undefined;
}
return `${error.name}: ${error.message}\n${error.stack}`;
}
return value;
};
if (value && typeof value === 'object' && typeof value.then === 'function')
return value.then(safeJson);
return safeJson(value);
}
}

View File

@ -16,17 +16,16 @@
import * as types from './types';
import * as dom from './dom';
import { helper } from './helper';
import * as utilityScriptSource from './generated/utilityScriptSource';
import { InnerLogger } from './logger';
import * as debugSupport from './debug/debugSupport';
import { RemoteObject, serializeAsCallArgument } from './remoteObject';
export interface ExecutionContextDelegate {
evaluate(context: ExecutionContext, returnByValue: boolean, pageFunction: string | Function, ...args: any[]): Promise<any>;
rawEvaluate(pageFunction: string): Promise<RemoteObject>;
getProperties(handle: JSHandle): Promise<Map<string, JSHandle>>;
releaseHandle(handle: JSHandle): Promise<void>;
handleToString(handle: JSHandle, includeType: boolean): string;
handleJSONValue<T>(handle: JSHandle<T>): Promise<T>;
}
@ -68,26 +67,26 @@ export class ExecutionContext {
return this._utilityScriptPromise;
}
createHandle(remoteObject: any): JSHandle {
createHandle(remoteObject: RemoteObject): JSHandle {
return new JSHandle(this, remoteObject);
}
}
export type RemoteObject = {
type?: string,
subtype?: string,
objectId?: string,
value?: any
};
export class JSHandle<T = any> {
readonly _context: ExecutionContext;
readonly _remoteObject: RemoteObject;
_disposed = false;
readonly _objectId: string | undefined;
readonly _value: any;
private _type: string;
constructor(context: ExecutionContext, remoteObject: RemoteObject) {
this._context = context;
this._remoteObject = remoteObject;
this._objectId = remoteObject.objectId;
// Remote objects for primitive (or unserializable) objects carry value.
this._value = potentiallyUnserializableValue(remoteObject);
// WebKit does not have a 'promise' type.
const isPromise = remoteObject.className === 'Promise';
this._type = isPromise ? 'promise' : remoteObject.subtype || remoteObject.type || 'object';
}
async evaluate<R, Arg>(pageFunction: types.FuncOn<T, Arg, R>, arg: Arg): Promise<R>;
@ -133,16 +132,26 @@ export class JSHandle<T = any> {
await this._context._delegate.releaseHandle(this);
}
_handleToString(includeType: boolean): string {
if (this._objectId)
return 'JSHandle@' + this._type;
return (includeType ? 'JSHandle:' : '') + this._value;
}
toString(): string {
return this._context._delegate.handleToString(this, true /* includeType */);
return this._handleToString(true);
}
}
export async function prepareFunctionCall<T>(
type CallArgument = {
value?: any,
objectId?: string
}
export async function prepareFunctionCall(
pageFunction: Function,
context: ExecutionContext,
args: any[],
toCallArgumentIfNeeded: (value: any) => { handle?: T, value?: any }): Promise<{ functionText: string, values: any[], handles: T[], dispose: () => void }> {
args: any[]): Promise<{ functionText: string, values: any[], handles: CallArgument[], dispose: () => void }> {
const originalText = pageFunction.toString();
let functionText = originalText;
@ -163,78 +172,55 @@ export async function prepareFunctionCall<T>(
}
}
const guids: string[] = [];
const handles: (Promise<JSHandle | T>)[] = [];
const handles: (Promise<JSHandle>)[] = [];
const toDispose: Promise<JSHandle>[] = [];
const pushHandle = (handle: Promise<JSHandle | T>): string => {
const guid = helper.guid();
guids.push(guid);
const pushHandle = (handle: Promise<JSHandle>): number => {
handles.push(handle);
return guid;
return handles.length - 1;
};
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._disposed)
throw new Error('JSHandle is disposed!');
const adopted = context.adoptIfNeeded(arg);
if (adopted === null)
return pushHandle(Promise.resolve(arg));
toDispose.push(adopted);
return pushHandle(adopted);
}
const { handle, value } = toCallArgumentIfNeeded(arg);
if (handle)
return pushHandle(Promise.resolve(handle));
return value;
};
args = args.map(arg => visit(arg, 100));
if (error)
throw new Error(error);
const resolved = await Promise.all(handles);
const resultHandles: T[] = [];
for (let i = 0; i < resolved.length; i++) {
const handle = resolved[i];
args = args.map(arg => serializeAsCallArgument(arg, (handle: any): { h?: number, fallThrough?: any } => {
if (handle instanceof JSHandle) {
if (handle._context !== context)
throw new Error('JSHandles can be evaluated only in the context they were created!');
resultHandles.push(toCallArgumentIfNeeded(handle).handle!);
} else {
resultHandles.push(handle);
if (!handle._objectId)
return { fallThrough: handle._value };
if (handle._disposed)
throw new Error('JSHandle is disposed!');
const adopted = context.adoptIfNeeded(handle);
if (adopted === null)
return { h: pushHandle(Promise.resolve(handle)) };
toDispose.push(adopted);
return { h: pushHandle(adopted) };
}
return { fallThrough: handle };
}));
const resultHandles: CallArgument[] = [];
for (const handle of await Promise.all(handles)) {
if (handle._context !== context)
throw new Error('JSHandles can be evaluated only in the context they were created!');
resultHandles.push({ objectId: handle._objectId });
}
const dispose = () => {
toDispose.map(handlePromise => handlePromise.then(handle => handle.dispose()));
};
functionText += await debugSupport.generateSourceMapUrl(originalText, functionText);
return { functionText, values: [ args.length, ...args, guids.length, ...guids ], handles: resultHandles, dispose };
return { functionText, values: [ args.length, ...args ], handles: resultHandles, dispose };
}
function potentiallyUnserializableValue(remoteObject: RemoteObject): any {
const value = remoteObject.value;
let unserializableValue = remoteObject.unserializableValue;
if (remoteObject.type === 'number' && value === null)
unserializableValue = remoteObject.description;
if (!unserializableValue)
return value;
if (unserializableValue === 'NaN')
return NaN;
if (unserializableValue === 'Infinity')
return Infinity;
if (unserializableValue === '-Infinity')
return -Infinity;
if (unserializableValue === '-0')
return -0;
return undefined;
}

147
src/remoteObject.ts Normal file
View File

@ -0,0 +1,147 @@
/**
* 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.
*/
// This file can't have dependencies, it is a part of the utility script.
export type RemoteObject = {
type?: string,
subtype?: string,
className?: string,
objectId?: string,
value?: any,
unserializableValue?: string
description?: string
};
export function parseEvaluationResultValue(value: any, handles: any[] = []): any {
// { type: 'undefined' } does not even have value.
if (value === 'undefined')
return undefined;
if (typeof value === 'object') {
if (value.v === 'undefined')
return undefined;
if (value.v === null)
return null;
if (value.v === 'NaN')
return NaN;
if (value.v === 'Infinity')
return Infinity;
if (value.v === '-Infinity')
return -Infinity;
if (value.v === '-0')
return -0;
if (value.d)
return new Date(value.d);
if (value.r)
return new RegExp(value.r[0], value.r[1]);
if (value.a)
return value.a.map((a: any) => parseEvaluationResultValue(a, handles));
if (value.o) {
for (const name of Object.keys(value.o))
value.o[name] = parseEvaluationResultValue(value.o[name], handles);
return value.o;
}
if (typeof value.h === 'number')
return handles[value.h];
}
return value;
}
export function serializeAsCallArgument(value: any, jsHandleSerializer: (value: any) => { fallThrough?: any }): any {
return serialize(value, jsHandleSerializer, new Set());
}
function serialize(value: any, jsHandleSerializer: (value: any) => { fallThrough?: any }, visited: Set<any>): any {
if (value && typeof value === 'object' && typeof value.then === 'function')
return value;
const result = jsHandleSerializer(value);
if ('fallThrough' in result)
value = result.fallThrough;
else
return result;
if (visited.has(value))
throw new Error('Argument is a circular structure');
if (typeof value === 'symbol')
return { v: 'undefined' };
if (Object.is(value, undefined))
return { v: 'undefined' };
if (Object.is(value, null))
return { v: null };
if (Object.is(value, NaN))
return { v: 'NaN' };
if (Object.is(value, Infinity))
return { v: 'Infinity' };
if (Object.is(value, -Infinity))
return { v: '-Infinity' };
if (Object.is(value, -0))
return { v: '-0' };
if (isPrimitiveValue(value))
return value;
if (value instanceof Error) {
const error = value;
if ('captureStackTrace' in global.Error) {
// v8
return error.stack;
}
return `${error.name}: ${error.message}\n${error.stack}`;
}
if (value instanceof Date)
return { d: value.toJSON() };
if (value instanceof RegExp)
return { r: [ value.source, value.flags ] };
if (Array.isArray(value)) {
const result = [];
visited.add(value);
for (let i = 0; i < value.length; ++i)
result.push(serialize(value[i], jsHandleSerializer, visited));
visited.delete(value);
return { a: result };
}
if (typeof value === 'object') {
const result: any = {};
visited.add(value);
for (const name of Object.keys(value)) {
let item;
try {
item = value[name];
} catch (e) {
continue; // native bindings will throw sometimes
}
if (name === 'toJSON' && typeof item === 'function')
result[name] = {};
else
result[name] = serialize(item, jsHandleSerializer, visited);
}
visited.delete(value);
return { o: result };
}
}
export function isPrimitiveValue(value: any): boolean {
switch (typeof value) {
case 'boolean':
case 'number':
case 'string':
return true;
default:
return false;
}
}

View File

@ -19,7 +19,7 @@ import { Protocol } from './protocol';
import * as dom from '../dom';
export async function getAccessibilityTree(session: WKSession, needle?: dom.ElementHandle) {
const objectId = needle ? needle._remoteObject.objectId : undefined;
const objectId = needle ? needle._objectId : undefined;
const {axNode} = await session.send('Page.accessibilitySnapshot', { objectId });
const tree = new WKAXNode(axNode);
return {

View File

@ -17,12 +17,10 @@
import { WKSession, isSwappedOutError } from './wkConnection';
import { helper } from '../helper';
import { valueFromRemoteObject, releaseObject } from './wkProtocolHelper';
import { Protocol } from './protocol';
import * as js from '../javascript';
import * as debugSupport from '../debug/debugSupport';
type MaybeCallArgument = Protocol.Runtime.CallArgument | { unserializable: any };
import { RemoteObject, parseEvaluationResultValue } from '../remoteObject';
export class WKExecutionContext implements js.ExecutionContextDelegate {
private readonly _session: WKSession;
@ -42,7 +40,7 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
this._contextDestroyedCallback();
}
async rawEvaluate(expression: string): Promise<js.RemoteObject> {
async rawEvaluate(expression: string): Promise<RemoteObject> {
const contextId = this._contextId;
const response = await this._session.send('Runtime.evaluate', {
expression: debugSupport.ensureSourceUrl(expression),
@ -71,8 +69,8 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
if (!returnByValue)
return context.createHandle(response.result);
if (response.result.objectId)
return await this._returnObjectByValue(response.result.objectId);
return valueFromRemoteObject(response.result);
return await this._returnObjectByValue(context, response.result.objectId);
return parseEvaluationResultValue(response.result.value);
} catch (error) {
if (isSwappedOutError(error) || error.message.includes('Missing injected script for given'))
throw new Error('Execution context was destroyed, most likely because of a navigation.');
@ -86,8 +84,10 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
const functionDeclaration = `function (returnByValue, pageFunction) { return this.evaluate(returnByValue, pageFunction); }` + debugSupport.generateSourceUrl();
return await this._session.send('Runtime.callFunctionOn', {
functionDeclaration,
objectId: utilityScript._remoteObject.objectId!,
arguments: [ { value: returnByValue }, { value: debugSupport.ensureSourceUrl(pageFunction) } ],
objectId: utilityScript._objectId!,
arguments: [
{ value: returnByValue },
{ value: debugSupport.ensureSourceUrl(pageFunction) } ],
returnByValue: false, // We need to return real Promise if that is a promise.
emulateUserGesture: true
});
@ -96,27 +96,19 @@ 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.`);
const { functionText, values, handles, dispose } = await 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 = value._remoteObject;
if (!remoteObject.objectId && !Object.is(valueFromRemoteObject(remoteObject), remoteObject.value))
return { handle: { unserializable: value } };
if (!remoteObject.objectId)
return { handle: { value: valueFromRemoteObject(remoteObject) } };
return { handle: { objectId: remoteObject.objectId } };
}
return { value };
});
const { functionText, values, handles, dispose } = await js.prepareFunctionCall(pageFunction, context, args);
try {
const utilityScript = await context.utilityScript();
const callParams = this._serializeFunctionAndArguments(functionText, values, handles, returnByValue);
return await this._session.send('Runtime.callFunctionOn', {
functionDeclaration: callParams.functionText + debugSupport.generateSourceUrl(),
objectId: utilityScript._remoteObject.objectId!,
arguments: callParams.callArguments,
functionDeclaration: `function (...args) { return this.callFunction(...args) }` + debugSupport.generateSourceUrl(),
objectId: utilityScript._objectId!,
arguments: [
{ value: returnByValue },
{ value: functionText },
...values.map(value => ({ value })),
...handles,
],
returnByValue: false, // We need to return real Promise if that is a promise.
emulateUserGesture: true
});
@ -125,56 +117,19 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
}
}
private _serializeFunctionAndArguments(originalText: string, values: any[], handles: MaybeCallArgument[], returnByValue: boolean): { functionText: string, callArguments: Protocol.Runtime.CallArgument[]} {
const callArguments: Protocol.Runtime.CallArgument[] = values.map(value => ({ value }));
let functionText = `function (returnByValue, functionText, ...args) { return this.callFunction(returnByValue, functionText, ...args); }`;
if (handles.some(handle => 'unserializable' in handle)) {
const paramStrings = [];
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('a[' + callArguments.length + ']');
callArguments.push(handle);
}
}
functionText = `function (returnByValue, functionText, ...a) { return this.callFunction(returnByValue, functionText, ${paramStrings.join(',')}); }`;
} else {
callArguments.push(...(handles as Protocol.Runtime.CallArgument[]));
}
return { functionText, callArguments: [ { value: returnByValue }, { value: originalText }, ...callArguments ] };
function unserializableToString(arg: any) {
if (Object.is(arg, -0))
return '-0';
if (Object.is(arg, Infinity))
return 'Infinity';
if (Object.is(arg, -Infinity))
return '-Infinity';
if (Object.is(arg, NaN))
return 'NaN';
if (arg instanceof js.JSHandle) {
const remoteObj = arg._remoteObject;
if (!remoteObj.objectId)
return valueFromRemoteObject(remoteObj);
}
throw new Error('Unsupported value: ' + arg + ' (' + (typeof arg) + ')');
}
}
private async _returnObjectByValue(objectId: Protocol.Runtime.RemoteObjectId): Promise<any> {
private async _returnObjectByValue(context: js.ExecutionContext, objectId: Protocol.Runtime.RemoteObjectId): Promise<any> {
// This is different from handleJSONValue in that it does not throw.
try {
const utilityScript = await context.utilityScript();
const serializeResponse = await this._session.send('Runtime.callFunctionOn', {
// Serialize object using standard JSON implementation to correctly pass 'undefined'.
functionDeclaration: 'function(){return this}\n' + debugSupport.generateSourceUrl(),
objectId: objectId,
functionDeclaration: 'object => object' + debugSupport.generateSourceUrl(),
objectId: utilityScript._objectId!,
arguments: [ { objectId } ],
returnByValue: true
});
if (serializeResponse.wasThrown)
return undefined;
return serializeResponse.result.value;
return parseEvaluationResultValue(serializeResponse.result.value);
} catch (e) {
if (isSwappedOutError(e))
return contextDestroyedResult;
@ -183,7 +138,7 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
}
async getProperties(handle: js.JSHandle): Promise<Map<string, js.JSHandle>> {
const objectId = handle._remoteObject.objectId;
const objectId = handle._objectId;
if (!objectId)
return new Map();
const response = await this._session.send('Runtime.getProperties', {
@ -192,7 +147,7 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
});
const result = new Map();
for (const property of response.properties) {
if (!property.enumerable)
if (!property.enumerable || !property.value)
continue;
result.set(property.name, handle._context.createHandle(property.value));
}
@ -200,32 +155,25 @@ export class WKExecutionContext implements js.ExecutionContextDelegate {
}
async releaseHandle(handle: js.JSHandle): Promise<void> {
await releaseObject(this._session, handle._remoteObject);
if (!handle._objectId)
return;
await this._session.send('Runtime.releaseObject', {objectId: handle._objectId}).catch(error => {});
}
async handleJSONValue<T>(handle: js.JSHandle<T>): Promise<T> {
const remoteObject = handle._remoteObject;
if (remoteObject.objectId) {
if (handle._objectId) {
const utilityScript = await handle._context.utilityScript();
const response = await this._session.send('Runtime.callFunctionOn', {
functionDeclaration: 'function() { return this; }' + debugSupport.generateSourceUrl(),
objectId: remoteObject.objectId,
functionDeclaration: 'function (object) { return this.jsonValue(true, object); }' + debugSupport.generateSourceUrl(),
objectId: utilityScript._objectId!,
arguments: [ { objectId: handle._objectId } ],
returnByValue: true
});
return valueFromRemoteObject(response.result);
if (response.wasThrown)
throw new Error('Evaluation failed: ' + response.result.description);
return parseEvaluationResultValue(response.result.value);
}
return valueFromRemoteObject(remoteObject);
}
handleToString(handle: js.JSHandle, includeType: boolean): string {
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.
if (object.className === 'Promise')
type = 'promise';
return 'JSHandle@' + type;
}
return (includeType ? 'JSHandle:' : '') + valueFromRemoteObject(object);
return handle._value;
}
}

View File

@ -710,7 +710,7 @@ export class WKPage implements PageDelegate {
async getContentFrame(handle: dom.ElementHandle): Promise<frames.Frame | null> {
const nodeInfo = await this._session.send('DOM.describeNode', {
objectId: toRemoteObject(handle).objectId!
objectId: handle._objectId!
});
if (!nodeInfo.contentFrameId)
return null;
@ -718,11 +718,10 @@ export class WKPage implements PageDelegate {
}
async getOwnerFrame(handle: dom.ElementHandle): Promise<string | null> {
const remoteObject = toRemoteObject(handle);
if (!remoteObject.objectId)
if (!handle._objectId)
return null;
const nodeInfo = await this._session.send('DOM.describeNode', {
objectId: remoteObject.objectId
objectId: handle._objectId
});
return nodeInfo.ownerFrameId || null;
}
@ -752,7 +751,7 @@ export class WKPage implements PageDelegate {
async scrollRectIntoViewIfNeeded(handle: dom.ElementHandle, rect?: types.Rect): Promise<'success' | 'invisible'> {
return await this._session.send('DOM.scrollIntoViewIfNeeded', {
objectId: toRemoteObject(handle).objectId!,
objectId: handle._objectId!,
rect,
}).then(() => 'success' as const).catch(e => {
if (e instanceof Error && e.message.includes('Node does not have a layout object'))
@ -772,7 +771,7 @@ export class WKPage implements PageDelegate {
async getContentQuads(handle: dom.ElementHandle): Promise<types.Quad[] | null> {
const result = await this._session.send('DOM.getContentQuads', {
objectId: toRemoteObject(handle).objectId!
objectId: handle._objectId!
}).catch(logError(this._page));
if (!result)
return null;
@ -789,13 +788,13 @@ export class WKPage implements PageDelegate {
}
async setInputFiles(handle: dom.ElementHandle<HTMLInputElement>, files: types.FilePayload[]): Promise<void> {
const objectId = toRemoteObject(handle).objectId!;
const objectId = handle._objectId!;
await this._session.send('DOM.setInputFiles', { objectId, files: dom.toFileTransferPayload(files) });
}
async adoptElementHandle<T extends Node>(handle: dom.ElementHandle<T>, to: dom.FrameExecutionContext): Promise<dom.ElementHandle<T>> {
const result = await this._session.send('DOM.resolveNode', {
objectId: toRemoteObject(handle).objectId,
objectId: handle._objectId!,
executionContextId: (to._delegate as WKExecutionContext)._contextId
}).catch(logError(this._page));
if (!result || result.object.subtype === 'null')
@ -927,7 +926,3 @@ export class WKPage implements PageDelegate {
await this._pageProxySession.send('Emulation.resetPermissions', {});
}
}
function toRemoteObject(handle: dom.ElementHandle): Protocol.Runtime.RemoteObject {
return handle._remoteObject as Protocol.Runtime.RemoteObject;
}

View File

@ -1,53 +0,0 @@
/**
* Copyright 2017 Google Inc. All rights reserved.
* Modifications 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.
*/
import { assert } from '../helper';
import { WKSession } from './wkConnection';
import { Protocol } from './protocol';
import * as js from '../javascript';
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) {
switch (remoteObject.description) {
case 'NaN':
return NaN;
case 'Infinity':
return Infinity;
case '-Infinity':
return -Infinity;
default:
throw new Error('Unsupported unserializable value: ' + remoteObject.description);
}
} else if (remoteObject.value === 0) {
switch (remoteObject.description) {
case '-0':
return -0;
}
}
}
return remoteObject.value;
}
export async function releaseObject(client: WKSession, remoteObject: js.RemoteObject) {
if (!remoteObject.objectId)
return;
await client.send('Runtime.releaseObject', {objectId: remoteObject.objectId}).catch(error => {});
}

View File

@ -40,6 +40,44 @@ describe('Page.evaluate', function() {
const result = await page.evaluate(a => a, -Infinity);
expect(Object.is(result, -Infinity)).toBe(true);
});
it('should roundtrip unserializable values', async({page}) => {
const value = {
infinity: Infinity,
nInfinity: -Infinity,
nZero: -0,
nan: NaN,
};
const result = await page.evaluate(value => value, value);
expect(result).toEqual(value);
});
it('should roundtrip promise to value', async({page}) => {
{
const result = await page.evaluate(value => Promise.resolve(value), null);
expect(result === null).toBeTruthy();
}
{
const result = await page.evaluate(value => Promise.resolve(value), Infinity);
expect(result === Infinity).toBeTruthy();
}
{
const result = await page.evaluate(value => Promise.resolve(value), -0);
expect(result === -0).toBeTruthy();
}
{
const result = await page.evaluate(value => Promise.resolve(value), undefined);
expect(result === undefined).toBeTruthy();
}
});
it('should roundtrip promise to unserializable values', async({page}) => {
const value = {
infinity: Infinity,
nInfinity: -Infinity,
nZero: -0,
nan: NaN,
};
const result = await page.evaluate(value => Promise.resolve(value), value);
expect(result).toEqual(value);
});
it('should transfer arrays', async({page, server}) => {
const result = await page.evaluate(a => a, [1, 2, 3]);
expect(result).toEqual([1,2,3]);
@ -61,7 +99,7 @@ describe('Page.evaluate', function() {
expect(await page.evaluate('globalVar')).toBe(123);
});
it('should return undefined for objects with symbols', async({page, server}) => {
expect(await page.evaluate(() => [Symbol('foo4')])).toBe(undefined);
expect(await page.evaluate(() => [Symbol('foo4')])).toEqual([undefined]);
expect(await page.evaluate(() => {
const a = { };
a[Symbol('foo4')] = 42;
@ -69,7 +107,7 @@ describe('Page.evaluate', function() {
})).toEqual({});
expect(await page.evaluate(() => {
return { foo: [{ a: Symbol('foo4') }] };
})).toBe(undefined);
})).toEqual({ foo: [ { a: undefined } ] });
});
it('should work with function shorthands', async({page, server}) => {
const a = {
@ -287,7 +325,8 @@ describe('Page.evaluate', function() {
const result = await page.evaluate(() => ({abc: 123}));
expect(result).toEqual({abc: 123});
});
it('should await promise from popup', async function({page, server}) {
it.fail(FFOX)('should await promise from popup', async function({page, server}) {
// Something is wrong about the way Firefox waits for the chained promise
await page.goto(server.EMPTY_PAGE);
const result = await page.evaluate(() => {
const win = window.open('about:blank');
@ -339,19 +378,29 @@ describe('Page.evaluate', function() {
const error = await page.evaluate(`new Error('error message')`);
expect(error).toContain('Error: error message');
});
it('should evaluate date as {}', async({page}) => {
const result = await page.evaluate(() => ({ date: new Date() }));
expect(result).toEqual({ date: {} });
it('should evaluate date', async({page}) => {
const result = await page.evaluate(() => ({ date: new Date('2020-05-27T01:31:38.506Z') }));
expect(result).toEqual({ date: new Date('2020-05-27T01:31:38.506Z') });
});
it('should jsonValue() date as {}', async({page}) => {
const resultHandle = await page.evaluateHandle(() => ({ date: new Date() }));
expect(await resultHandle.jsonValue()).toEqual({ date: {} });
it('should roundtrip date', async({page}) => {
const date = new Date('2020-05-27T01:31:38.506Z');
const result = await page.evaluate(date => date, date);
expect(result.toUTCString()).toEqual(date.toUTCString());
});
it.fail(FFOX)('should not use toJSON when evaluating', async({page, server}) => {
it('should roundtrip regex', async({page}) => {
const regex = /hello/im;
const result = await page.evaluate(regex => regex, regex);
expect(result.toString()).toEqual(regex.toString());
});
it('should jsonValue() date', async({page}) => {
const resultHandle = await page.evaluateHandle(() => ({ date: new Date('2020-05-27T01:31:38.506Z') }));
expect(await resultHandle.jsonValue()).toEqual({ date: new Date('2020-05-27T01:31:38.506Z') });
});
it('should not use toJSON when evaluating', async({page, server}) => {
const result = await page.evaluate(() => ({ toJSON: () => 'string', data: 'data' }));
expect(result).toEqual({ data: 'data', toJSON: {} });
});
it.fail(FFOX)('should not use toJSON in jsonValue', async({page, server}) => {
it('should not use toJSON in jsonValue', async({page, server}) => {
const resultHandle = await page.evaluateHandle(() => ({ toJSON: () => 'string', data: 'data' }));
expect(await resultHandle.jsonValue()).toEqual({ data: 'data', toJSON: {} });
});

View File

@ -58,16 +58,6 @@ describe('Page.evaluateHandle', function() {
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;
@ -137,7 +127,23 @@ describe('JSHandle.getProperty', function() {
expect(await nullHandle.jsonValue()).toEqual(null);
const emptyhandle = await aHandle.getProperty('empty');
expect(String(await emptyhandle.jsonValue())).toEqual('undefined');
})
});
it('should work with unserializable values', async({page, server}) => {
const aHandle = await page.evaluateHandle(() => ({
infinity: Infinity,
nInfinity: -Infinity,
nan: NaN,
nzero: -0
}));
const infinityHandle = await aHandle.getProperty('infinity');
expect(await infinityHandle.jsonValue()).toEqual(Infinity);
const nInfinityHandle = await aHandle.getProperty('nInfinity');
expect(await nInfinityHandle.jsonValue()).toEqual(-Infinity);
const nanHandle = await aHandle.getProperty('nan');
expect(String(await nanHandle.jsonValue())).toEqual('NaN');
const nzeroHandle = await aHandle.getProperty('nzero');
expect(await nzeroHandle.jsonValue()).toEqual(-0);
});
});
describe('JSHandle.jsonValue', function() {
@ -155,12 +161,7 @@ describe('JSHandle.jsonValue', function() {
const windowHandle = await page.evaluateHandle('window');
let error = null;
await windowHandle.jsonValue().catch(e => error = e);
if (WEBKIT)
expect(error.message).toContain('Object has too long reference chain');
else if (CHROMIUM)
expect(error.message).toContain('Object reference chain is too long');
else if (FFOX)
expect(error.message).toContain('Object is not serializable');
expect(error.message).toContain('Argument is a circular structure');
});
it('should work with tricky values', async({page, server}) => {
const aHandle = await page.evaluateHandle(() => ({a: 1}));