Bug 1623581 - [remote] Add "awaitPromise" support to Runtime.evaluate. r=remote-protocol-reviewers,maja_zf
authorHenrik Skupin <mail@hskupin.info>
Tue, 24 Mar 2020 20:34:54 +0000
changeset 520288 c92f0faccdb5609b7f355bce915dd83511bd5d44
parent 520287 36920c4ef85039eda7cdd7e5c03f7757f047dc9b
child 520289 9f72cf155767e1d5e22c66d40a92969228d216cc
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 "awaitPromise" support to Runtime.evaluate. r=remote-protocol-reviewers,maja_zf Differential Revision: https://phabricator.services.mozilla.com/D67842
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
@@ -201,53 +201,61 @@ class Runtime extends ContentProcessDoma
   }
 
   /**
    * Evaluate expression on global object.
    *
    * @param {Object} options
    * @param {string} options.expression
    *     Expression to evaluate.
-   * @param {boolean=} options.awaitPromise [unsupported]
+   * @param {boolean=} options.awaitPromise
    *     Whether execution should `await` for resulting value
    *     and return once awaited promise is resolved.
    * @param {number=} options.contextId
    *     Specifies in which execution context to perform evaluation.
    *     If the parameter is omitted the evaluation will be performed
    *     in the context of the inspected page.
    * @param {boolean=} options.returnByValue
    *     Whether the result is expected to be a JSON object
    *     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, returnByValue = false } = options;
+    const {
+      expression,
+      awaitPromise = false,
+      contextId,
+      returnByValue = false,
+    } = options;
 
     if (typeof expression != "string") {
       throw new Error("expression: string value expected");
     }
+    if (!["undefined", "boolean"].includes(typeof options.awaitPromise)) {
+      throw new TypeError("awaitPromise: boolean 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, returnByValue);
+    return context.evaluate(expression, awaitPromise, 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,46 +71,66 @@ class ExecutionContext {
     return this._remoteObjects.delete(id);
   }
 
   /**
    * Evaluate a Javascript expression.
    *
    * @param {String} expression
    *   The JS expression to evaluate against the JS context.
+   * @param {boolean} options.awaitPromise
+   *     Whether execution should `await` for resulting value
+   *     and return once awaited promise is resolved.
    * @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, returnByValue) {
+  async evaluate(expression, awaitPromise, returnByValue) {
     let rv = this._debuggee.executeInGlobal(expression);
     if (!rv) {
       return {
         exceptionDetails: {
           text: "Evaluation terminated!",
         },
       };
     }
 
     if (rv.throw) {
       return this._returnError(rv.throw);
     }
 
-    let result;
+    let result = rv.return;
+
+    if (result && result.isPromise && awaitPromise) {
+      if (result.promiseState === "fulfilled") {
+        result = result.promiseValue;
+      } else if (result.promiseState === "rejected") {
+        return this._returnError(result.promiseReason);
+      } else {
+        try {
+          const promiseResult = await result.unsafeDereference();
+          result = this._debuggee.makeDebuggeeValue(promiseResult);
+        } catch (e) {
+          // The promise has been rejected
+          return this._returnError(e);
+        }
+      }
+    }
+
     if (returnByValue) {
       result = this._toRemoteObjectByValue(result);
     } else {
-      result = this._toRemoteObject(rv.return);
+      result = this._toRemoteObject(result);
     }
 
     return { result };
   }
 
   /**
    * Given a Debugger.Object reference for an Exception, return a JSON object
    * describing the exception by following CDP ExceptionDetails specification.
--- a/remote/test/browser/runtime/browser_evaluate.js
+++ b/remote/test/browser/runtime/browser_evaluate.js
@@ -1,15 +1,160 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 const TEST_DOC = toDataURL("default-test-page");
 
+add_task(async function awaitPromiseInvalidTypes({ client }) {
+  const { Runtime } = client;
+
+  await enableRuntime(client);
+
+  for (const awaitPromise of [null, 1, "foo", [], {}]) {
+    let errorThrown = "";
+    try {
+      await Runtime.evaluate({
+        expression: "",
+        awaitPromise,
+      });
+    } catch (e) {
+      errorThrown = e.message;
+    }
+    ok(errorThrown.includes("awaitPromise: boolean value expected"));
+  }
+});
+
+add_task(async function awaitPromiseResolve({ client }) {
+  const { Runtime } = client;
+
+  await enableRuntime(client);
+
+  const { result } = await Runtime.evaluate({
+    expression: "Promise.resolve(42)",
+    awaitPromise: true,
+  });
+
+  is(result.type, "number", "The type is correct");
+  is(result.subtype, null, "The subtype is null for numbers");
+  is(result.value, 42, "The result is the promise's resolution");
+});
+
+add_task(async function awaitPromiseReject({ client }) {
+  const { Runtime } = client;
+
+  await enableRuntime(client);
+
+  const { exceptionDetails } = await Runtime.evaluate({
+    expression: "Promise.reject(42)",
+    awaitPromise: true,
+  });
+  // TODO: Implement all values for exceptionDetails (bug 1548480)
+  is(
+    exceptionDetails.exception.value,
+    42,
+    "The result is the promise's rejection"
+  );
+});
+
+add_task(async function awaitPromiseDelayedResolve({ client }) {
+  const { Runtime } = client;
+
+  await enableRuntime(client);
+
+  const { result } = await Runtime.evaluate({
+    expression: "new Promise(r => setTimeout(() => r(42), 0))",
+    awaitPromise: true,
+  });
+  is(result.type, "number", "The type is correct");
+  is(result.subtype, null, "The subtype is null for numbers");
+  is(result.value, 42, "The result is the promise's resolution");
+});
+
+add_task(async function awaitPromiseDelayedReject({ client }) {
+  const { Runtime } = client;
+
+  await enableRuntime(client);
+
+  const { exceptionDetails } = await Runtime.evaluate({
+    expression: "new Promise((_,r) => setTimeout(() => r(42), 0))",
+    awaitPromise: true,
+  });
+  is(
+    exceptionDetails.exception.value,
+    42,
+    "The result is the promise's rejection"
+  );
+});
+
+add_task(async function awaitPromiseResolveWithoutWait({ client }) {
+  const { Runtime } = client;
+
+  await enableRuntime(client);
+
+  const { result } = await Runtime.evaluate({
+    expression: "Promise.resolve(42)",
+    awaitPromise: false,
+  });
+
+  is(result.type, "object", "The type is correct");
+  is(result.subtype, "promise", "The subtype is promise");
+  ok(!!result.objectId, "We got the object id for the promise");
+  ok(!result.value, "We do not receive any value");
+});
+
+add_task(async function awaitPromiseDelayedResolveWithoutWait({ client }) {
+  const { Runtime } = client;
+
+  await enableRuntime(client);
+
+  const { result } = await Runtime.evaluate({
+    expression: "new Promise(r => setTimeout(() => r(42), 0))",
+    awaitPromise: false,
+  });
+
+  is(result.type, "object", "The type is correct");
+  is(result.subtype, "promise", "The subtype is promise");
+  ok(!!result.objectId, "We got the object id for the promise");
+  ok(!result.value, "We do not receive any value");
+});
+
+add_task(async function awaitPromiseRejectWithoutWait({ client }) {
+  const { Runtime } = client;
+
+  await enableRuntime(client);
+
+  const { result } = await Runtime.evaluate({
+    expression: "Promise.reject(42)",
+    awaitPromise: false,
+  });
+
+  is(result.type, "object", "The type is correct");
+  is(result.subtype, "promise", "The subtype is promise");
+  ok(!!result.objectId, "We got the object id for the promise");
+  ok(!result.exceptionDetails, "We do not receive any exception");
+});
+
+add_task(async function awaitPromiseDelayedRejectWithoutWait({ client }) {
+  const { Runtime } = client;
+
+  await enableRuntime(client);
+
+  const { result } = await Runtime.evaluate({
+    expression: "new Promise((_,r) => setTimeout(() => r(42), 0))",
+    awaitPromise: false,
+  });
+
+  is(result.type, "object", "The type is correct");
+  is(result.subtype, "promise", "The subtype is promise");
+  ok(!!result.objectId, "We got the object id for the promise");
+  ok(!result.exceptionDetails, "We do not receive any exception");
+});
+
 add_task(async function contextIdInvalidValue({ client }) {
   const { Runtime } = client;
 
   let errorThrown = "";
   try {
     await Runtime.evaluate({ expression: "", contextId: -1 });
   } catch (e) {
     errorThrown = e.message;