Bug 1157253: Port ListenerProxy to use Proxy instead of __noSuchMethod__
authorAndreas Tolfsen <ato@mozilla.com>
Thu, 23 Apr 2015 16:59:12 +0100
changeset 241275 c30b5528ce96015ce43677cee69bb1ce98ad4470
parent 241274 91110ad64d3223bf3164f76c1cbfff78f170375b
child 241276 a7f8556dca8954da69209bcf4603a17c44482c7a
push id59070
push useratolfsen@mozilla.com
push dateMon, 27 Apr 2015 20:15:39 +0000
treeherdermozilla-inbound@c30b5528ce96 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1157253
milestone40.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 1157253: Port ListenerProxy to use Proxy instead of __noSuchMethod__ r=chmanchester
testing/marionette/driver.js
--- a/testing/marionette/driver.js
+++ b/testing/marionette/driver.js
@@ -91,40 +91,83 @@ this.Context.fromString = function(s) {
  * browsing context (content) and responses will be provided back as
  * promises.
  *
  * The argument sequence is serialised and passed as an array, unless it
  * consists of a single object type that isn't null, in which case it's
  * passed literally.  The latter specialisation is temporary to achieve
  * backwards compatibility with listener.js.
  *
- * @param {function(): nsIMessageManager} mmFn
+ * @param {function(): (nsIMessageSender|nsIMessageBroadcaster)} mmFn
  *     Function returning the current message manager.
  * @param {function(string, Object, number)} sendAsyncFn
  *     Callback for sending async messages to the current listener.
  * @param {function(): BrowserObj} curBrowserFn
  *     Function that returns the current browser.
  */
 let ListenerProxy = function(mmFn, sendAsyncFn, curBrowserFn) {
+  let sender = new ContentSender(mmFn, sendAsyncFn, curBrowserFn);
+  let handler = {
+    set: (obj, prop, val) => { obj[prop] = val; return true; },
+    get: (obj, prop) => (...args) => obj.send(prop, args),
+  };
+  return new Proxy(sender, handler);
+};
+
+/**
+ * The ContentSender allows one to make synchronous calls to the
+ * message listener of the content frame of the current browsing context.
+ *
+ * Presumptions about the responses from content space are made so we
+ * can provide a nicer API on top of the message listener primitives that
+ * make calls from chrome- to content space seem synchronous by leveraging
+ * promises.
+ *
+ * The promise is guaranteed not to resolve until the execution of the
+ * command in content space is complete.
+ *
+ * @param {function(): (nsIMessageSender|nsIMessageBroadcaster)} mmFn
+ *     Function returning the current message manager.
+ * @param {function(string, Object, number)} sendAsyncFn
+ *     Callback for sending async messages to the current listener.
+ * @param {function(): BrowserObj} curBrowserFn
+ *     Function that returns the current browser.
+ */
+let ContentSender = function(mmFn, sendAsyncFn, curBrowserFn) {
   this.curCmdId = null;
   this.sendAsync = sendAsyncFn;
 
   this.mmFn_ = mmFn;
   this.curBrowserFn_ = curBrowserFn;
 };
 
-Object.defineProperty(ListenerProxy.prototype, "mm", {
+Object.defineProperty(ContentSender.prototype, "mm", {
   get: function() { return this.mmFn_(); }
 });
 
-Object.defineProperty(ListenerProxy.prototype, "curBrowser", {
+Object.defineProperty(ContentSender.prototype, "curBrowser", {
   get: function() { return this.curBrowserFn_(); }
 });
 
-ListenerProxy.prototype.__noSuchMethod__ = function*(name, args) {
+/**
+ * Call registered function in the frame script environment of the
+ * current browsing context's content frame.
+ *
+ * @param {string} name
+ *     Function to call in the listener, e.g. for "Marionette:foo8",
+ *     use "foo".
+ * @param {Array} args
+ *     Argument list to pass the function.  If args has a single entry
+ *     that is an object, we assume it's an old style dispatch, and
+ *     the object will passed literally.
+ *
+ * @return {Promise}
+ *     A promise that resolves to the result of the command.
+ */
+ContentSender.prototype.send = function(name, args) {
   const ok = "Marionette:ok";
   const val = "Marionette:done";
   const err = "Marionette:error";
 
   let proxy = new Promise((resolve, reject) => {
     let removeListeners = (name, fn) => {
       let rmFn = msg => {
         if (this.isOutOfSync(msg.json.command_id)) {
@@ -164,31 +207,30 @@ ListenerProxy.prototype.__noSuchMethod__
       resolve();
     }.bind(this);
 
     // start content process listeners, and install observers for global-
     // and tab modal dialogues
     listeners.add();
     modal.addHandler(handleDialog);
 
-    // convert to array if passed arguments
-    let msg;
-    if (args.length == 1 && typeof args[0] == "object" && args[0] !== null) {
+    // new style dispatches are arrays of arguments, old style dispatches
+    // are key-value objects
+    let msg = args;
+    if (args.length == 1 && typeof args[0] == "object") {
       msg = args[0];
-    } else {
-      msg = Array.prototype.slice.call(args);
     }
 
     this.sendAsync(name, msg, this.curCmdId);
   });
 
   return proxy;
 };
 
-ListenerProxy.prototype.isOutOfSync = function(id) {
+ContentSender.prototype.isOutOfSync = function(id) {
   return this.curCmdId !== id;
 };
 
 /**
  * Implements (parts of) the W3C WebDriver protocol.  GeckoDriver lives
  * in the chrome context and mediates content calls to the listener via
  * ListenerProxy.
  *
@@ -263,17 +305,17 @@ this.GeckoDriver = function(appName, dev
     // Proprietary extensions
     "XULappId" : Services.appinfo.ID,
     "appBuildId" : Services.appinfo.appBuildID,
     "device": device,
     "version": Services.appinfo.version
   };
 
   this.mm = globalMessageManager;
-  this.listener = new ListenerProxy(
+  this.listener = ListenerProxy(
       () => this.mm,
       this.sendAsync.bind(this),
       () => this.curBrowser);
 
   this.dialog = null;
   let handleDialog = (subject, topic) => {
     let winr;
     if (topic == modal.COMMON_DIALOG_LOADED) {
@@ -1276,17 +1318,17 @@ GeckoDriver.prototype.get = function(cmd
   let url = cmd.parameters.url;
 
   switch (this.context) {
     case Context.CONTENT:
       // If a remoteness update interrupts our page load, this will never return
       // We need to re-issue this request to correctly poll for readyState and
       // send errors.
       this.curBrowser.pendingCommands.push(() => {
-        cmd.parameters.command_id = this.listener.curCmdId;
+        cmd.parameters.command_id = cmd.id;
         this.mm.broadcastAsyncMessage(
             "Marionette:pollForReadyState" + this.curBrowser.curFrameId,
             cmd.parameters);
       });
       yield this.listener.get({url: url, pageTimeout: this.pageTimeout});
       break;
 
     case Context.CHROME: