scriptEvaluator.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.ScriptEvaluator = exports.SHARED_ID_DIVIDER = void 0;
  4. const protocol_js_1 = require("../../../protocol/protocol.js");
  5. const channelProxy_js_1 = require("./channelProxy.js");
  6. // As `script.evaluate` wraps call into serialization script, `lineNumber`
  7. // should be adjusted.
  8. const CALL_FUNCTION_STACKTRACE_LINE_OFFSET = 1;
  9. const EVALUATE_STACKTRACE_LINE_OFFSET = 0;
  10. exports.SHARED_ID_DIVIDER = '_element_';
  11. class ScriptEvaluator {
  12. #eventManager;
  13. constructor(eventManager) {
  14. this.#eventManager = eventManager;
  15. }
  16. /**
  17. * Gets the string representation of an object. This is equivalent to
  18. * calling toString() on the object value.
  19. * @param cdpObject CDP remote object representing an object.
  20. * @param realm
  21. * @return string The stringified object.
  22. */
  23. static async stringifyObject(cdpObject, realm) {
  24. const stringifyResult = await realm.cdpClient.sendCommand('Runtime.callFunctionOn', {
  25. functionDeclaration: String((obj) => {
  26. return String(obj);
  27. }),
  28. awaitPromise: false,
  29. arguments: [cdpObject],
  30. returnByValue: true,
  31. executionContextId: realm.executionContextId,
  32. });
  33. return stringifyResult.result.value;
  34. }
  35. /**
  36. * Serializes a given CDP object into BiDi, keeping references in the
  37. * target's `globalThis`.
  38. * @param cdpRemoteObject CDP remote object to be serialized.
  39. * @param resultOwnership Indicates desired ResultOwnership.
  40. * @param realm
  41. */
  42. async serializeCdpObject(cdpRemoteObject, resultOwnership, realm) {
  43. const arg = ScriptEvaluator.#cdpRemoteObjectToCallArgument(cdpRemoteObject);
  44. const cdpWebDriverValue = await realm.cdpClient.sendCommand('Runtime.callFunctionOn', {
  45. functionDeclaration: String((obj) => obj),
  46. awaitPromise: false,
  47. arguments: [arg],
  48. serializationOptions: {
  49. serialization: 'deep',
  50. },
  51. executionContextId: realm.executionContextId,
  52. });
  53. return realm.cdpToBidiValue(cdpWebDriverValue, resultOwnership);
  54. }
  55. async scriptEvaluate(realm, expression, awaitPromise, resultOwnership, serializationOptions) {
  56. if (![0, null, undefined].includes(serializationOptions.maxDomDepth))
  57. throw new Error('serializationOptions.maxDomDepth other than 0 or null is not supported');
  58. const cdpEvaluateResult = await realm.cdpClient.sendCommand('Runtime.evaluate', {
  59. contextId: realm.executionContextId,
  60. expression,
  61. awaitPromise,
  62. serializationOptions: {
  63. serialization: 'deep',
  64. ...(serializationOptions.maxObjectDepth === undefined ||
  65. serializationOptions.maxObjectDepth === null
  66. ? {}
  67. : { maxDepth: serializationOptions.maxObjectDepth }),
  68. },
  69. });
  70. if (cdpEvaluateResult.exceptionDetails) {
  71. // Serialize exception details.
  72. return {
  73. exceptionDetails: await this.#serializeCdpExceptionDetails(cdpEvaluateResult.exceptionDetails, EVALUATE_STACKTRACE_LINE_OFFSET, resultOwnership, realm),
  74. type: 'exception',
  75. realm: realm.realmId,
  76. };
  77. }
  78. return {
  79. type: 'success',
  80. result: realm.cdpToBidiValue(cdpEvaluateResult, resultOwnership),
  81. realm: realm.realmId,
  82. };
  83. }
  84. async callFunction(realm, functionDeclaration, _this, _arguments, awaitPromise, resultOwnership, serializationOptions) {
  85. if (![0, null, undefined].includes(serializationOptions.maxDomDepth))
  86. throw new Error('serializationOptions.maxDomDepth other than 0 or null is not supported');
  87. const callFunctionAndSerializeScript = `(...args)=>{ return _callFunction((\n${functionDeclaration}\n), args);
  88. function _callFunction(f, args) {
  89. const deserializedThis = args.shift();
  90. const deserializedArgs = args;
  91. return f.apply(deserializedThis, deserializedArgs);
  92. }}`;
  93. const thisAndArgumentsList = [
  94. await this.#deserializeToCdpArg(_this, realm),
  95. ];
  96. thisAndArgumentsList.push(...(await Promise.all(_arguments.map(async (a) => {
  97. return this.#deserializeToCdpArg(a, realm);
  98. }))));
  99. let cdpCallFunctionResult;
  100. try {
  101. cdpCallFunctionResult = await realm.cdpClient.sendCommand('Runtime.callFunctionOn', {
  102. functionDeclaration: callFunctionAndSerializeScript,
  103. awaitPromise,
  104. arguments: thisAndArgumentsList,
  105. serializationOptions: {
  106. serialization: 'deep',
  107. ...(serializationOptions.maxObjectDepth === undefined ||
  108. serializationOptions.maxObjectDepth === null
  109. ? {}
  110. : { maxDepth: serializationOptions.maxObjectDepth }),
  111. },
  112. executionContextId: realm.executionContextId,
  113. });
  114. }
  115. catch (e) {
  116. // Heuristic to determine if the problem is in the argument.
  117. // The check can be done on the `deserialization` step, but this approach
  118. // helps to save round-trips.
  119. if (e.code === -32000 &&
  120. [
  121. 'Could not find object with given id',
  122. 'Argument should belong to the same JavaScript world as target object',
  123. 'Invalid remote object id',
  124. ].includes(e.message)) {
  125. throw new protocol_js_1.Message.NoSuchHandleException('Handle was not found.');
  126. }
  127. throw e;
  128. }
  129. if (cdpCallFunctionResult.exceptionDetails) {
  130. // Serialize exception details.
  131. return {
  132. exceptionDetails: await this.#serializeCdpExceptionDetails(cdpCallFunctionResult.exceptionDetails, CALL_FUNCTION_STACKTRACE_LINE_OFFSET, resultOwnership, realm),
  133. type: 'exception',
  134. realm: realm.realmId,
  135. };
  136. }
  137. return {
  138. type: 'success',
  139. result: realm.cdpToBidiValue(cdpCallFunctionResult, resultOwnership),
  140. realm: realm.realmId,
  141. };
  142. }
  143. static #cdpRemoteObjectToCallArgument(cdpRemoteObject) {
  144. if (cdpRemoteObject.objectId !== undefined) {
  145. return { objectId: cdpRemoteObject.objectId };
  146. }
  147. if (cdpRemoteObject.unserializableValue !== undefined) {
  148. return { unserializableValue: cdpRemoteObject.unserializableValue };
  149. }
  150. return { value: cdpRemoteObject.value };
  151. }
  152. async #deserializeToCdpArg(argumentValue, realm) {
  153. if ('sharedId' in argumentValue) {
  154. const [navigableId, rawBackendNodeId] = argumentValue.sharedId.split(exports.SHARED_ID_DIVIDER);
  155. const backendNodeId = parseInt(rawBackendNodeId ?? '');
  156. if (isNaN(backendNodeId) ||
  157. backendNodeId === undefined ||
  158. navigableId === undefined) {
  159. throw new protocol_js_1.Message.NoSuchNodeException(`SharedId "${argumentValue.sharedId}" was not found.`);
  160. }
  161. if (realm.navigableId !== navigableId) {
  162. throw new protocol_js_1.Message.NoSuchNodeException(`SharedId "${argumentValue.sharedId}" belongs to different document. Current document is ${realm.navigableId}.`);
  163. }
  164. try {
  165. const obj = await realm.cdpClient.sendCommand('DOM.resolveNode', {
  166. backendNodeId,
  167. executionContextId: realm.executionContextId,
  168. });
  169. // TODO(#375): Release `obj.object.objectId` after using.
  170. return { objectId: obj.object.objectId };
  171. }
  172. catch (e) {
  173. // Heuristic to detect "no such node" exception. Based on the specific
  174. // CDP implementation.
  175. if (e.code === -32000 && e.message === 'No node with given id found') {
  176. throw new protocol_js_1.Message.NoSuchNodeException(`SharedId "${argumentValue.sharedId}" was not found.`);
  177. }
  178. throw e;
  179. }
  180. }
  181. if ('handle' in argumentValue) {
  182. return { objectId: argumentValue.handle };
  183. }
  184. switch (argumentValue.type) {
  185. // Primitive Protocol Value
  186. // https://w3c.github.io/webdriver-bidi/#data-types-protocolValue-primitiveProtocolValue
  187. case 'undefined':
  188. return { unserializableValue: 'undefined' };
  189. case 'null':
  190. return { unserializableValue: 'null' };
  191. case 'string':
  192. return { value: argumentValue.value };
  193. case 'number':
  194. if (argumentValue.value === 'NaN') {
  195. return { unserializableValue: 'NaN' };
  196. }
  197. else if (argumentValue.value === '-0') {
  198. return { unserializableValue: '-0' };
  199. }
  200. else if (argumentValue.value === 'Infinity') {
  201. return { unserializableValue: 'Infinity' };
  202. }
  203. else if (argumentValue.value === '-Infinity') {
  204. return { unserializableValue: '-Infinity' };
  205. }
  206. return {
  207. value: argumentValue.value,
  208. };
  209. case 'boolean':
  210. return { value: Boolean(argumentValue.value) };
  211. case 'bigint':
  212. return {
  213. unserializableValue: `BigInt(${JSON.stringify(argumentValue.value)})`,
  214. };
  215. case 'date':
  216. return {
  217. unserializableValue: `new Date(Date.parse(${JSON.stringify(argumentValue.value)}))`,
  218. };
  219. case 'regexp':
  220. return {
  221. unserializableValue: `new RegExp(${JSON.stringify(argumentValue.value.pattern)}, ${JSON.stringify(argumentValue.value.flags)})`,
  222. };
  223. case 'map': {
  224. // TODO: If none of the nested keys and values has a remote
  225. // reference, serialize to `unserializableValue` without CDP roundtrip.
  226. const keyValueArray = await this.#flattenKeyValuePairs(argumentValue.value, realm);
  227. const argEvalResult = await realm.cdpClient.sendCommand('Runtime.callFunctionOn', {
  228. functionDeclaration: String((...args) => {
  229. const result = new Map();
  230. for (let i = 0; i < args.length; i += 2) {
  231. result.set(args[i], args[i + 1]);
  232. }
  233. return result;
  234. }),
  235. awaitPromise: false,
  236. arguments: keyValueArray,
  237. returnByValue: false,
  238. executionContextId: realm.executionContextId,
  239. });
  240. // TODO(#375): Release `argEvalResult.result.objectId` after using.
  241. return { objectId: argEvalResult.result.objectId };
  242. }
  243. case 'object': {
  244. // TODO: If none of the nested keys and values has a remote
  245. // reference, serialize to `unserializableValue` without CDP roundtrip.
  246. const keyValueArray = await this.#flattenKeyValuePairs(argumentValue.value, realm);
  247. const argEvalResult = await realm.cdpClient.sendCommand('Runtime.callFunctionOn', {
  248. functionDeclaration: String((...args) => {
  249. const result = {};
  250. for (let i = 0; i < args.length; i += 2) {
  251. // Key should be either `string`, `number`, or `symbol`.
  252. const key = args[i];
  253. result[key] = args[i + 1];
  254. }
  255. return result;
  256. }),
  257. awaitPromise: false,
  258. arguments: keyValueArray,
  259. returnByValue: false,
  260. executionContextId: realm.executionContextId,
  261. });
  262. // TODO(#375): Release `argEvalResult.result.objectId` after using.
  263. return { objectId: argEvalResult.result.objectId };
  264. }
  265. case 'array': {
  266. // TODO: If none of the nested items has a remote reference,
  267. // serialize to `unserializableValue` without CDP roundtrip.
  268. const args = await this.#flattenValueList(argumentValue.value, realm);
  269. const argEvalResult = await realm.cdpClient.sendCommand('Runtime.callFunctionOn', {
  270. functionDeclaration: String((...args) => {
  271. return args;
  272. }),
  273. awaitPromise: false,
  274. arguments: args,
  275. returnByValue: false,
  276. executionContextId: realm.executionContextId,
  277. });
  278. // TODO(#375): Release `argEvalResult.result.objectId` after using.
  279. return { objectId: argEvalResult.result.objectId };
  280. }
  281. case 'set': {
  282. // TODO: if none of the nested items has a remote reference,
  283. // serialize to `unserializableValue` without CDP roundtrip.
  284. const args = await this.#flattenValueList(argumentValue.value, realm);
  285. const argEvalResult = await realm.cdpClient.sendCommand('Runtime.callFunctionOn', {
  286. functionDeclaration: String((...args) => {
  287. return new Set(args);
  288. }),
  289. awaitPromise: false,
  290. arguments: args,
  291. returnByValue: false,
  292. executionContextId: realm.executionContextId,
  293. });
  294. // TODO(#375): Release `argEvalResult.result.objectId` after using.
  295. return { objectId: argEvalResult.result.objectId };
  296. }
  297. case 'channel': {
  298. const channelProxy = new channelProxy_js_1.ChannelProxy(argumentValue.value);
  299. const channelProxySendMessageHandle = await channelProxy.init(realm, this.#eventManager);
  300. return { objectId: channelProxySendMessageHandle };
  301. }
  302. // TODO(#375): Dispose of nested objects.
  303. default:
  304. throw new Error(`Value ${JSON.stringify(argumentValue)} is not deserializable.`);
  305. }
  306. }
  307. async #flattenKeyValuePairs(mapping, realm) {
  308. const keyValueArray = [];
  309. for (const [key, value] of mapping) {
  310. let keyArg;
  311. if (typeof key === 'string') {
  312. // Key is a string.
  313. keyArg = { value: key };
  314. }
  315. else {
  316. // Key is a serialized value.
  317. keyArg = await this.#deserializeToCdpArg(key, realm);
  318. }
  319. const valueArg = await this.#deserializeToCdpArg(value, realm);
  320. keyValueArray.push(keyArg);
  321. keyValueArray.push(valueArg);
  322. }
  323. return keyValueArray;
  324. }
  325. async #flattenValueList(list, realm) {
  326. return Promise.all(list.map((value) => this.#deserializeToCdpArg(value, realm)));
  327. }
  328. async #serializeCdpExceptionDetails(cdpExceptionDetails, lineOffset, resultOwnership, realm) {
  329. const callFrames = cdpExceptionDetails.stackTrace?.callFrames.map((frame) => ({
  330. url: frame.url,
  331. functionName: frame.functionName,
  332. // As `script.evaluate` wraps call into serialization script, so
  333. // `lineNumber` should be adjusted.
  334. lineNumber: frame.lineNumber - lineOffset,
  335. columnNumber: frame.columnNumber,
  336. }));
  337. const exception = await this.serializeCdpObject(
  338. // Exception should always be there.
  339. cdpExceptionDetails.exception, resultOwnership, realm);
  340. const text = await ScriptEvaluator.stringifyObject(cdpExceptionDetails.exception, realm);
  341. return {
  342. exception,
  343. columnNumber: cdpExceptionDetails.columnNumber,
  344. // As `script.evaluate` wraps call into serialization script, so
  345. // `lineNumber` should be adjusted.
  346. lineNumber: cdpExceptionDetails.lineNumber - lineOffset,
  347. stackTrace: {
  348. callFrames: callFrames ?? [],
  349. },
  350. text: text || cdpExceptionDetails.text,
  351. };
  352. }
  353. }
  354. exports.ScriptEvaluator = ScriptEvaluator;
  355. //# sourceMappingURL=scriptEvaluator.js.map