123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355 |
- "use strict";
- Object.defineProperty(exports, "__esModule", { value: true });
- exports.ScriptEvaluator = exports.SHARED_ID_DIVIDER = void 0;
- const protocol_js_1 = require("../../../protocol/protocol.js");
- const channelProxy_js_1 = require("./channelProxy.js");
- // As `script.evaluate` wraps call into serialization script, `lineNumber`
- // should be adjusted.
- const CALL_FUNCTION_STACKTRACE_LINE_OFFSET = 1;
- const EVALUATE_STACKTRACE_LINE_OFFSET = 0;
- exports.SHARED_ID_DIVIDER = '_element_';
- class ScriptEvaluator {
- #eventManager;
- constructor(eventManager) {
- this.#eventManager = eventManager;
- }
- /**
- * Gets the string representation of an object. This is equivalent to
- * calling toString() on the object value.
- * @param cdpObject CDP remote object representing an object.
- * @param realm
- * @return string The stringified object.
- */
- static async stringifyObject(cdpObject, realm) {
- const stringifyResult = await realm.cdpClient.sendCommand('Runtime.callFunctionOn', {
- functionDeclaration: String((obj) => {
- return String(obj);
- }),
- awaitPromise: false,
- arguments: [cdpObject],
- returnByValue: true,
- executionContextId: realm.executionContextId,
- });
- return stringifyResult.result.value;
- }
- /**
- * Serializes a given CDP object into BiDi, keeping references in the
- * target's `globalThis`.
- * @param cdpRemoteObject CDP remote object to be serialized.
- * @param resultOwnership Indicates desired ResultOwnership.
- * @param realm
- */
- async serializeCdpObject(cdpRemoteObject, resultOwnership, realm) {
- const arg = ScriptEvaluator.#cdpRemoteObjectToCallArgument(cdpRemoteObject);
- const cdpWebDriverValue = await realm.cdpClient.sendCommand('Runtime.callFunctionOn', {
- functionDeclaration: String((obj) => obj),
- awaitPromise: false,
- arguments: [arg],
- serializationOptions: {
- serialization: 'deep',
- },
- executionContextId: realm.executionContextId,
- });
- return realm.cdpToBidiValue(cdpWebDriverValue, resultOwnership);
- }
- async scriptEvaluate(realm, expression, awaitPromise, resultOwnership, serializationOptions) {
- if (![0, null, undefined].includes(serializationOptions.maxDomDepth))
- throw new Error('serializationOptions.maxDomDepth other than 0 or null is not supported');
- const cdpEvaluateResult = await realm.cdpClient.sendCommand('Runtime.evaluate', {
- contextId: realm.executionContextId,
- expression,
- awaitPromise,
- serializationOptions: {
- serialization: 'deep',
- ...(serializationOptions.maxObjectDepth === undefined ||
- serializationOptions.maxObjectDepth === null
- ? {}
- : { maxDepth: serializationOptions.maxObjectDepth }),
- },
- });
- if (cdpEvaluateResult.exceptionDetails) {
- // Serialize exception details.
- return {
- exceptionDetails: await this.#serializeCdpExceptionDetails(cdpEvaluateResult.exceptionDetails, EVALUATE_STACKTRACE_LINE_OFFSET, resultOwnership, realm),
- type: 'exception',
- realm: realm.realmId,
- };
- }
- return {
- type: 'success',
- result: realm.cdpToBidiValue(cdpEvaluateResult, resultOwnership),
- realm: realm.realmId,
- };
- }
- async callFunction(realm, functionDeclaration, _this, _arguments, awaitPromise, resultOwnership, serializationOptions) {
- if (![0, null, undefined].includes(serializationOptions.maxDomDepth))
- throw new Error('serializationOptions.maxDomDepth other than 0 or null is not supported');
- const callFunctionAndSerializeScript = `(...args)=>{ return _callFunction((\n${functionDeclaration}\n), args);
- function _callFunction(f, args) {
- const deserializedThis = args.shift();
- const deserializedArgs = args;
- return f.apply(deserializedThis, deserializedArgs);
- }}`;
- const thisAndArgumentsList = [
- await this.#deserializeToCdpArg(_this, realm),
- ];
- thisAndArgumentsList.push(...(await Promise.all(_arguments.map(async (a) => {
- return this.#deserializeToCdpArg(a, realm);
- }))));
- let cdpCallFunctionResult;
- try {
- cdpCallFunctionResult = await realm.cdpClient.sendCommand('Runtime.callFunctionOn', {
- functionDeclaration: callFunctionAndSerializeScript,
- awaitPromise,
- arguments: thisAndArgumentsList,
- serializationOptions: {
- serialization: 'deep',
- ...(serializationOptions.maxObjectDepth === undefined ||
- serializationOptions.maxObjectDepth === null
- ? {}
- : { maxDepth: serializationOptions.maxObjectDepth }),
- },
- executionContextId: realm.executionContextId,
- });
- }
- catch (e) {
- // Heuristic to determine if the problem is in the argument.
- // The check can be done on the `deserialization` step, but this approach
- // helps to save round-trips.
- if (e.code === -32000 &&
- [
- 'Could not find object with given id',
- 'Argument should belong to the same JavaScript world as target object',
- 'Invalid remote object id',
- ].includes(e.message)) {
- throw new protocol_js_1.Message.NoSuchHandleException('Handle was not found.');
- }
- throw e;
- }
- if (cdpCallFunctionResult.exceptionDetails) {
- // Serialize exception details.
- return {
- exceptionDetails: await this.#serializeCdpExceptionDetails(cdpCallFunctionResult.exceptionDetails, CALL_FUNCTION_STACKTRACE_LINE_OFFSET, resultOwnership, realm),
- type: 'exception',
- realm: realm.realmId,
- };
- }
- return {
- type: 'success',
- result: realm.cdpToBidiValue(cdpCallFunctionResult, resultOwnership),
- realm: realm.realmId,
- };
- }
- static #cdpRemoteObjectToCallArgument(cdpRemoteObject) {
- if (cdpRemoteObject.objectId !== undefined) {
- return { objectId: cdpRemoteObject.objectId };
- }
- if (cdpRemoteObject.unserializableValue !== undefined) {
- return { unserializableValue: cdpRemoteObject.unserializableValue };
- }
- return { value: cdpRemoteObject.value };
- }
- async #deserializeToCdpArg(argumentValue, realm) {
- if ('sharedId' in argumentValue) {
- const [navigableId, rawBackendNodeId] = argumentValue.sharedId.split(exports.SHARED_ID_DIVIDER);
- const backendNodeId = parseInt(rawBackendNodeId ?? '');
- if (isNaN(backendNodeId) ||
- backendNodeId === undefined ||
- navigableId === undefined) {
- throw new protocol_js_1.Message.NoSuchNodeException(`SharedId "${argumentValue.sharedId}" was not found.`);
- }
- if (realm.navigableId !== navigableId) {
- throw new protocol_js_1.Message.NoSuchNodeException(`SharedId "${argumentValue.sharedId}" belongs to different document. Current document is ${realm.navigableId}.`);
- }
- try {
- const obj = await realm.cdpClient.sendCommand('DOM.resolveNode', {
- backendNodeId,
- executionContextId: realm.executionContextId,
- });
- // TODO(#375): Release `obj.object.objectId` after using.
- return { objectId: obj.object.objectId };
- }
- catch (e) {
- // Heuristic to detect "no such node" exception. Based on the specific
- // CDP implementation.
- if (e.code === -32000 && e.message === 'No node with given id found') {
- throw new protocol_js_1.Message.NoSuchNodeException(`SharedId "${argumentValue.sharedId}" was not found.`);
- }
- throw e;
- }
- }
- if ('handle' in argumentValue) {
- return { objectId: argumentValue.handle };
- }
- switch (argumentValue.type) {
- // Primitive Protocol Value
- // https://w3c.github.io/webdriver-bidi/#data-types-protocolValue-primitiveProtocolValue
- case 'undefined':
- return { unserializableValue: 'undefined' };
- case 'null':
- return { unserializableValue: 'null' };
- case 'string':
- return { value: argumentValue.value };
- case 'number':
- if (argumentValue.value === 'NaN') {
- return { unserializableValue: 'NaN' };
- }
- else if (argumentValue.value === '-0') {
- return { unserializableValue: '-0' };
- }
- else if (argumentValue.value === 'Infinity') {
- return { unserializableValue: 'Infinity' };
- }
- else if (argumentValue.value === '-Infinity') {
- return { unserializableValue: '-Infinity' };
- }
- return {
- value: argumentValue.value,
- };
- case 'boolean':
- return { value: Boolean(argumentValue.value) };
- case 'bigint':
- return {
- unserializableValue: `BigInt(${JSON.stringify(argumentValue.value)})`,
- };
- case 'date':
- return {
- unserializableValue: `new Date(Date.parse(${JSON.stringify(argumentValue.value)}))`,
- };
- case 'regexp':
- return {
- unserializableValue: `new RegExp(${JSON.stringify(argumentValue.value.pattern)}, ${JSON.stringify(argumentValue.value.flags)})`,
- };
- case 'map': {
- // TODO: If none of the nested keys and values has a remote
- // reference, serialize to `unserializableValue` without CDP roundtrip.
- const keyValueArray = await this.#flattenKeyValuePairs(argumentValue.value, realm);
- const argEvalResult = await realm.cdpClient.sendCommand('Runtime.callFunctionOn', {
- functionDeclaration: String((...args) => {
- const result = new Map();
- for (let i = 0; i < args.length; i += 2) {
- result.set(args[i], args[i + 1]);
- }
- return result;
- }),
- awaitPromise: false,
- arguments: keyValueArray,
- returnByValue: false,
- executionContextId: realm.executionContextId,
- });
- // TODO(#375): Release `argEvalResult.result.objectId` after using.
- return { objectId: argEvalResult.result.objectId };
- }
- case 'object': {
- // TODO: If none of the nested keys and values has a remote
- // reference, serialize to `unserializableValue` without CDP roundtrip.
- const keyValueArray = await this.#flattenKeyValuePairs(argumentValue.value, realm);
- const argEvalResult = await realm.cdpClient.sendCommand('Runtime.callFunctionOn', {
- functionDeclaration: String((...args) => {
- const result = {};
- for (let i = 0; i < args.length; i += 2) {
- // Key should be either `string`, `number`, or `symbol`.
- const key = args[i];
- result[key] = args[i + 1];
- }
- return result;
- }),
- awaitPromise: false,
- arguments: keyValueArray,
- returnByValue: false,
- executionContextId: realm.executionContextId,
- });
- // TODO(#375): Release `argEvalResult.result.objectId` after using.
- return { objectId: argEvalResult.result.objectId };
- }
- case 'array': {
- // TODO: If none of the nested items has a remote reference,
- // serialize to `unserializableValue` without CDP roundtrip.
- const args = await this.#flattenValueList(argumentValue.value, realm);
- const argEvalResult = await realm.cdpClient.sendCommand('Runtime.callFunctionOn', {
- functionDeclaration: String((...args) => {
- return args;
- }),
- awaitPromise: false,
- arguments: args,
- returnByValue: false,
- executionContextId: realm.executionContextId,
- });
- // TODO(#375): Release `argEvalResult.result.objectId` after using.
- return { objectId: argEvalResult.result.objectId };
- }
- case 'set': {
- // TODO: if none of the nested items has a remote reference,
- // serialize to `unserializableValue` without CDP roundtrip.
- const args = await this.#flattenValueList(argumentValue.value, realm);
- const argEvalResult = await realm.cdpClient.sendCommand('Runtime.callFunctionOn', {
- functionDeclaration: String((...args) => {
- return new Set(args);
- }),
- awaitPromise: false,
- arguments: args,
- returnByValue: false,
- executionContextId: realm.executionContextId,
- });
- // TODO(#375): Release `argEvalResult.result.objectId` after using.
- return { objectId: argEvalResult.result.objectId };
- }
- case 'channel': {
- const channelProxy = new channelProxy_js_1.ChannelProxy(argumentValue.value);
- const channelProxySendMessageHandle = await channelProxy.init(realm, this.#eventManager);
- return { objectId: channelProxySendMessageHandle };
- }
- // TODO(#375): Dispose of nested objects.
- default:
- throw new Error(`Value ${JSON.stringify(argumentValue)} is not deserializable.`);
- }
- }
- async #flattenKeyValuePairs(mapping, realm) {
- const keyValueArray = [];
- for (const [key, value] of mapping) {
- let keyArg;
- if (typeof key === 'string') {
- // Key is a string.
- keyArg = { value: key };
- }
- else {
- // Key is a serialized value.
- keyArg = await this.#deserializeToCdpArg(key, realm);
- }
- const valueArg = await this.#deserializeToCdpArg(value, realm);
- keyValueArray.push(keyArg);
- keyValueArray.push(valueArg);
- }
- return keyValueArray;
- }
- async #flattenValueList(list, realm) {
- return Promise.all(list.map((value) => this.#deserializeToCdpArg(value, realm)));
- }
- async #serializeCdpExceptionDetails(cdpExceptionDetails, lineOffset, resultOwnership, realm) {
- const callFrames = cdpExceptionDetails.stackTrace?.callFrames.map((frame) => ({
- url: frame.url,
- functionName: frame.functionName,
- // As `script.evaluate` wraps call into serialization script, so
- // `lineNumber` should be adjusted.
- lineNumber: frame.lineNumber - lineOffset,
- columnNumber: frame.columnNumber,
- }));
- const exception = await this.serializeCdpObject(
- // Exception should always be there.
- cdpExceptionDetails.exception, resultOwnership, realm);
- const text = await ScriptEvaluator.stringifyObject(cdpExceptionDetails.exception, realm);
- return {
- exception,
- columnNumber: cdpExceptionDetails.columnNumber,
- // As `script.evaluate` wraps call into serialization script, so
- // `lineNumber` should be adjusted.
- lineNumber: cdpExceptionDetails.lineNumber - lineOffset,
- stackTrace: {
- callFrames: callFrames ?? [],
- },
- text: text || cdpExceptionDetails.text,
- };
- }
- }
- exports.ScriptEvaluator = ScriptEvaluator;
- //# sourceMappingURL=scriptEvaluator.js.map
|