Bug 1548098 - Implement Runtime.callFunctionOn's objectId argument. r=remote-protocol-reviewers,ato
authorAlexandre Poirot <poirot.alex@gmail.com>
Mon, 13 May 2019 16:11:49 +0000
changeset 532449 d1985ca4a9e2cc6d94366eea82150788f185305b
parent 532448 087726ba0d15387d6a18b152a1687c2b562b1806
child 532450 dac784d695b31fcffe11a840c2c08e287b467ee6
push id11268
push usercsabou@mozilla.com
push dateTue, 14 May 2019 15:24:22 +0000
treeherdermozilla-beta@5fb7fcd568d6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersremote-protocol-reviewers, ato
bugs1548098
milestone68.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 1548098 - Implement Runtime.callFunctionOn's objectId argument. r=remote-protocol-reviewers,ato Differential Revision: https://phabricator.services.mozilla.com/D30267
remote/domains/content/Runtime.jsm
remote/domains/content/runtime/ExecutionContext.jsm
remote/test/browser/browser_runtime_callFunctionOn.js
--- a/remote/domains/content/Runtime.jsm
+++ b/remote/domains/content/Runtime.jsm
@@ -73,19 +73,34 @@ class Runtime extends ContentProcessDoma
     if (typeof(request.expression) != "string") {
       throw new Error(`Expecting 'expression' attribute to be a string. ` +
         `But was: ${typeof(request.expression)}`);
     }
     return context.evaluate(request.expression);
   }
 
   callFunctionOn(request) {
-    const context = this.contexts.get(request.executionContextId);
-    if (!context) {
-      throw new Error(`Unable to find execution context with id: ${request.executionContextId}`);
+    let context = null;
+    // When an `objectId` is passed, we want to execute the function of a given object
+    // So we first have to find its ExecutionContext
+    if (request.objectId) {
+      for (const ctx of this.contexts.values()) {
+        if (ctx.hasRemoteObject(request.objectId)) {
+          context = ctx;
+          break;
+        }
+      }
+      if (!context) {
+        throw new Error(`Unable to get the context for object with id: ${request.objectId}`);
+      }
+    } else {
+      context = this.contexts.get(request.executionContextId);
+      if (!context) {
+        throw new Error(`Unable to find execution context with id: ${request.executionContextId}`);
+      }
     }
     if (typeof(request.functionDeclaration) != "string") {
       throw new Error("Expect 'functionDeclaration' attribute to be passed and be a string");
     }
     if (request.arguments && !Array.isArray(request.arguments)) {
       throw new Error("Expect 'arguments' to be an array");
     }
     if (request.returnByValue && typeof(request.returnByValue) != "boolean") {
--- a/remote/domains/content/runtime/ExecutionContext.jsm
+++ b/remote/domains/content/runtime/ExecutionContext.jsm
@@ -33,16 +33,20 @@ class ExecutionContext {
 
     this._remoteObjects = new Map();
   }
 
   destructor() {
     this._debugger.removeDebuggee(this._debuggee);
   }
 
+  hasRemoteObject(id) {
+    return this._remoteObjects.has(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.
@@ -85,17 +89,26 @@ class ExecutionContext {
     // If that isn't an Error, consider the exception as a JS value
     return {
       exceptionDetails: {
         exception: this._toRemoteObject(exception),
       },
     };
   }
 
-  async callFunctionOn(functionDeclaration, callArguments = [], returnByValue = false, awaitPromise = false) {
+  async callFunctionOn(functionDeclaration, callArguments = [], returnByValue = false, awaitPromise = false, objectId = null) {
+    // Map the given objectId to a JS reference.
+    let thisArg = null;
+    if (objectId) {
+      thisArg = this._remoteObjects.get(objectId);
+      if (!thisArg) {
+        throw new Error(`Unable to get target object with id: ${objectId}`);
+      }
+    }
+
     // First evaluate the function
     const fun = this._debuggee.executeInGlobal("(" + functionDeclaration + ")");
     if (!fun) {
       return {
         exceptionDetails: {
           text: "Evaluation terminated!",
         },
       };
@@ -104,17 +117,17 @@ class ExecutionContext {
       return this._returnError(fun.throw);
     }
 
     // Then map all input arguments, which are matching CDP's CallArguments type,
     // into JS values
     const args = callArguments.map(arg => this._fromCallArgument(arg));
 
     // Finally, call the function with these arguments
-    const rv = fun.return.apply(null, args);
+    const rv = fun.return.apply(thisArg, args);
     if (rv.throw) {
       return this._returnError(rv.throw);
     }
 
     let result = rv.return;
 
     if (result && result.isPromise && awaitPromise) {
       if (result.promiseState === "fulfilled") {
--- a/remote/test/browser/browser_runtime_callFunctionOn.js
+++ b/remote/test/browser/browser_runtime_callFunctionOn.js
@@ -28,16 +28,17 @@ add_task(async function() {
   ok(true, "CDP client has been instantiated");
 
   const firstContext = await testRuntimeEnable(client);
   const contextId = firstContext.id;
   await testObjectReferences(client, contextId);
   await testExceptions(client, contextId);
   await testReturnByValue(client, contextId);
   await testAwaitPromise(client, contextId);
+  await testObjectId(client, contextId);
 
   await client.close();
   ok(true, "The client is closed");
 
   BrowserTestUtils.removeTab(gBrowser.selectedTab);
 
   await RemoteAgent.close();
 });
@@ -202,8 +203,26 @@ async function testAwaitPromise({ Runtim
     functionDeclaration: "() => 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");
 }
+
+async function testObjectId({ Runtime }, contextId) {
+  // First create an object via Runtime.evaluate
+  const { result } = await Runtime.evaluate({ contextId, expression: "({ foo: 42 })" });
+  is(result.type, "object", "The type is correct");
+  is(result.subtype, null, "The subtype is null for objects");
+  ok(!!result.objectId, "Got an object id");
+
+  // Then apply a method on this object
+  const { result: result2 } = await Runtime.callFunctionOn({
+    executionContextId: contextId,
+    functionDeclaration: "function () { return this.foo; }",
+    objectId: result.objectId,
+  });
+  is(result2.type, "number", "The type is correct");
+  is(result2.subtype, null, "The subtype is null for numbers");
+  is(result2.value, 42, "We have a good proof that the function was ran against the target object");
+}