mirror of
https://github.com/microsoft/playwright.git
synced 2024-10-27 21:58:52 +03:00
fix(evaluate): consistently serialize json values (#2377)
This commit is contained in:
parent
609bc4cfb0
commit
e168fddac8
@ -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;
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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>>;
|
||||
}
|
||||
|
@ -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> {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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}};
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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
147
src/remoteObject.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 => {});
|
||||
}
|
||||
|
@ -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: {} });
|
||||
});
|
||||
|
@ -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}));
|
||||
|
Loading…
Reference in New Issue
Block a user