Bug 1623581 - [remote] Add "returnByValue" support to Runtime.evaluate. r=remote-protocol-reviewers,maja_zf
authorHenrik Skupin <mail@hskupin.info>
Tue, 24 Mar 2020 20:32:19 +0000
changeset 520287 36920c4ef85039eda7cdd7e5c03f7757f047dc9b
parent 520286 9f32172ae499e754c49403a797fed6fa4aa9fd78
child 520288 c92f0faccdb5609b7f355bce915dd83511bd5d44
push id37246
push useropoprus@mozilla.com
push dateWed, 25 Mar 2020 03:40:33 +0000
treeherdermozilla-central@14b59d4adc95 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersremote-protocol-reviewers, maja_zf
bugs1623581
milestone76.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1623581 - [remote] Add "returnByValue" support to Runtime.evaluate. r=remote-protocol-reviewers,maja_zf Differential Revision: https://phabricator.services.mozilla.com/D67841
remote/domains/content/Runtime.jsm
remote/domains/content/runtime/ExecutionContext.jsm
remote/test/browser/runtime/browser_evaluate.js
--- a/remote/domains/content/Runtime.jsm
+++ b/remote/domains/content/Runtime.jsm
@@ -218,33 +218,36 @@ class Runtime extends ContentProcessDoma
    *     that should be sent by value. Defaults to false.
    * @param {boolean=} options.userGesture [unsupported]
    *     Whether execution should be treated as initiated by user in the UI.
    *
    * @return {Object<RemoteObject, exceptionDetails>}
    *     The evaluation result, and optionally exception details.
    */
   evaluate(options = {}) {
-    const { expression, contextId } = options;
+    const { expression, contextId, returnByValue = false } = options;
 
     if (typeof expression != "string") {
       throw new Error("expression: string value expected");
     }
+    if (typeof returnByValue != "boolean") {
+      throw new Error("returnByValue: boolean value expected");
+    }
 
     let context;
     if (typeof contextId != "undefined") {
       context = this.contexts.get(contextId);
       if (!context) {
         throw new Error("Cannot find context with specified id");
       }
     } else {
       context = this._getDefaultContextForWindow();
     }
 
-    return context.evaluate(expression);
+    return context.evaluate(expression, returnByValue);
   }
 
   getProperties({ objectId, ownProperties }) {
     for (const ctx of this.contexts.values()) {
       const obj = ctx.getRemoteObject(objectId);
       if (typeof obj != "undefined") {
         return ctx.getProperties({ objectId, ownProperties });
       }
--- a/remote/domains/content/runtime/ExecutionContext.jsm
+++ b/remote/domains/content/runtime/ExecutionContext.jsm
@@ -71,37 +71,49 @@ class ExecutionContext {
     return this._remoteObjects.delete(id);
   }
 
   /**
    * Evaluate a Javascript expression.
    *
    * @param {String} expression
    *   The JS expression to evaluate against the JS context.
-   * @return {Object} A multi-form object depending if the execution succeed or failed.
-   *   If the expression failed to evaluate, it will return an object with an
-   *   `exceptionDetails` attribute matching the `ExceptionDetails` CDP type.
-   *   Otherwise it will return an object with `result` attribute whose type is
+   * @param {boolean} returnByValue
+   *     Whether the result is expected to be a JSON object
+   *     that should be sent by value.
+   *
+   * @return {Object} A multi-form object depending if the execution
+   *   succeed or failed. If the expression failed to evaluate,
+   *   it will return an object with an `exceptionDetails` attribute
+   *   matching the `ExceptionDetails` CDP type. Otherwise it will
+   *   return an object with `result` attribute whose type is
    *   `RemoteObject` CDP type.
    */
-  evaluate(expression) {
+  evaluate(expression, returnByValue) {
     let rv = this._debuggee.executeInGlobal(expression);
     if (!rv) {
       return {
         exceptionDetails: {
           text: "Evaluation terminated!",
         },
       };
     }
+
     if (rv.throw) {
       return this._returnError(rv.throw);
     }
-    return {
-      result: this._toRemoteObject(rv.return),
-    };
+
+    let result;
+    if (returnByValue) {
+      result = this._toRemoteObjectByValue(result);
+    } else {
+      result = this._toRemoteObject(rv.return);
+    }
+
+    return { result };
   }
 
   /**
    * Given a Debugger.Object reference for an Exception, return a JSON object
    * describing the exception by following CDP ExceptionDetails specification.
    */
   _returnError(exception) {
     if (
@@ -432,16 +444,17 @@ class ExecutionContext {
     };
   }
 
   /**
    * Convert a given `Debugger.Object` to an object.
    *
    * @param {Debugger.Object} obj
    *  The object to convert
+   *
    * @return {Object}
    *  The converted object
    */
   _serialize(debuggerObj) {
     const result = this._debuggee.executeInGlobalWithBindings(
       "JSON.stringify(e)",
       { e: debuggerObj }
     );
--- a/remote/test/browser/runtime/browser_evaluate.js
+++ b/remote/test/browser/runtime/browser_evaluate.js
@@ -147,16 +147,121 @@ add_task(async function returnAsObjectUn
     result,
     {
       type: "undefined",
     },
     "Undefined type is correct"
   );
 });
 
+add_task(async function returnByValueInvalidTypes({ client }) {
+  const { Runtime } = client;
+
+  await enableRuntime(client);
+
+  for (const returnByValue of [null, 1, "foo", [], {}]) {
+    let errorThrown = "";
+    try {
+      await Runtime.evaluate({
+        expression: "",
+        returnByValue,
+      });
+    } catch (e) {
+      errorThrown = e.message;
+    }
+    ok(errorThrown.includes("returnByValue: boolean value expected"));
+  }
+});
+
+add_task(async function returnByValue({ client }) {
+  const { Runtime } = client;
+
+  await enableRuntime(client);
+
+  const values = [
+    null,
+    42,
+    42.0,
+    "42",
+    true,
+    false,
+    { foo: true },
+    { foo: { bar: 42, str: "str", array: [1, 2, 3] } },
+    [42, "42", true],
+    [{ foo: true }],
+  ];
+
+  for (const value of values) {
+    const { result } = await Runtime.evaluate({
+      expression: `(${JSON.stringify(value)})`,
+      returnByValue: true,
+    });
+
+    Assert.deepEqual(
+      result,
+      {
+        type: typeof value,
+        value,
+        description: value != null ? value.toString() : value,
+      },
+      `Returned expected value for ${JSON.stringify(value)}`
+    );
+  }
+});
+
+add_task(async function returnByValueNotSerializable({ client }) {
+  const { Runtime } = client;
+
+  await enableRuntime(client);
+
+  const notSerializableNumbers = {
+    number: ["-0", "NaN", "Infinity", "-Infinity"],
+    bigint: ["42n"],
+  };
+
+  for (const type in notSerializableNumbers) {
+    for (const unserializableValue of notSerializableNumbers[type]) {
+      const { result } = await Runtime.evaluate({
+        expression: `(${unserializableValue})`,
+        returnByValue: true,
+      });
+
+      Assert.deepEqual(
+        result,
+        {
+          type,
+          unserializableValue,
+          description: unserializableValue,
+        },
+        `Returned expected value for ${JSON.stringify(unserializableValue)}`
+      );
+    }
+  }
+});
+
+// Test undefined individually as JSON.stringify doesn't return a string
+add_task(async function returnByValueUndefined({ client }) {
+  const { Runtime } = client;
+
+  await enableRuntime(client);
+
+  const { result } = await Runtime.evaluate({
+    expression: "undefined",
+    returnByValue: true,
+  });
+
+  Assert.deepEqual(
+    result,
+    {
+      type: "undefined",
+    },
+    "Undefined type is correct"
+  );
+});
+
 add_task(async function exceptionDetailsJavascriptError({ client }) {
   const { Runtime } = client;
 
   await enableRuntime(client);
 
   const { exceptionDetails } = await Runtime.evaluate({
     expression: "doesNotExists()",
   });